@trevonistrevon/pi-loop 0.4.11 → 0.5.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.
Files changed (87) hide show
  1. package/README.md +20 -9
  2. package/dist/commands/loop-command.d.ts +22 -0
  3. package/dist/commands/loop-command.js +148 -0
  4. package/dist/commands/tasks-command.d.ts +15 -0
  5. package/dist/commands/tasks-command.js +117 -0
  6. package/dist/coordinator.d.ts +35 -0
  7. package/dist/coordinator.js +56 -0
  8. package/dist/goal-coordinator.d.ts +22 -0
  9. package/dist/goal-coordinator.js +28 -0
  10. package/dist/goal-reducer.d.ts +89 -0
  11. package/dist/goal-reducer.js +181 -0
  12. package/dist/goal-store.d.ts +31 -0
  13. package/dist/goal-store.js +298 -0
  14. package/dist/goal-types.d.ts +82 -0
  15. package/dist/goal-types.js +1 -0
  16. package/dist/goal-verifier.d.ts +20 -0
  17. package/dist/goal-verifier.js +198 -0
  18. package/dist/index.js +130 -1087
  19. package/dist/loop-reducer.d.ts +63 -0
  20. package/dist/loop-reducer.js +67 -0
  21. package/dist/monitor-completion-coordinator.d.ts +10 -0
  22. package/dist/monitor-completion-coordinator.js +13 -0
  23. package/dist/monitor-manager.d.ts +2 -0
  24. package/dist/monitor-manager.js +107 -29
  25. package/dist/monitor-reducer.d.ts +82 -0
  26. package/dist/monitor-reducer.js +69 -0
  27. package/dist/notification-reducer.d.ts +81 -0
  28. package/dist/notification-reducer.js +65 -0
  29. package/dist/runtime/monitor-ondone-runtime.d.ts +13 -0
  30. package/dist/runtime/monitor-ondone-runtime.js +49 -0
  31. package/dist/runtime/notification-runtime.d.ts +34 -0
  32. package/dist/runtime/notification-runtime.js +152 -0
  33. package/dist/runtime/scope.d.ts +8 -0
  34. package/dist/runtime/scope.js +33 -0
  35. package/dist/runtime/session-runtime.d.ts +39 -0
  36. package/dist/runtime/session-runtime.js +110 -0
  37. package/dist/runtime/task-backlog-runtime.d.ts +36 -0
  38. package/dist/runtime/task-backlog-runtime.js +105 -0
  39. package/dist/runtime/task-rpc.d.ts +19 -0
  40. package/dist/runtime/task-rpc.js +118 -0
  41. package/dist/store.d.ts +7 -4
  42. package/dist/store.js +129 -49
  43. package/dist/task-backlog-coordinator.d.ts +12 -0
  44. package/dist/task-backlog-coordinator.js +22 -0
  45. package/dist/task-reducer.d.ts +66 -0
  46. package/dist/task-reducer.js +76 -0
  47. package/dist/task-store.d.ts +8 -4
  48. package/dist/task-store.js +102 -33
  49. package/dist/tools/loop-tools.d.ts +41 -0
  50. package/dist/tools/loop-tools.js +241 -0
  51. package/dist/tools/monitor-tools.d.ts +25 -0
  52. package/dist/tools/monitor-tools.js +110 -0
  53. package/dist/tools/native-task-tools.d.ts +15 -0
  54. package/dist/tools/native-task-tools.js +127 -0
  55. package/docs/architecture/goal-state-schema.md +505 -0
  56. package/docs/architecture/state-machine-migration.md +546 -0
  57. package/docs/architecture/state-machine-reducer-event-model.md +823 -0
  58. package/docs/architecture/state-machine-test-matrix.md +249 -0
  59. package/docs/architecture/state-machine-transition-map.md +436 -0
  60. package/package.json +1 -1
  61. package/src/commands/loop-command.ts +184 -0
  62. package/src/commands/tasks-command.ts +135 -0
  63. package/src/coordinator.ts +115 -0
  64. package/src/goal-coordinator.ts +58 -0
  65. package/src/goal-reducer.ts +280 -0
  66. package/src/goal-store.ts +315 -0
  67. package/src/goal-types.ts +104 -0
  68. package/src/goal-verifier.ts +241 -0
  69. package/src/index.ts +134 -1147
  70. package/src/loop-reducer.ts +148 -0
  71. package/src/monitor-completion-coordinator.ts +24 -0
  72. package/src/monitor-manager.ts +115 -27
  73. package/src/monitor-reducer.ts +166 -0
  74. package/src/notification-reducer.ts +155 -0
  75. package/src/runtime/monitor-ondone-runtime.ts +75 -0
  76. package/src/runtime/notification-runtime.ts +212 -0
  77. package/src/runtime/scope.ts +37 -0
  78. package/src/runtime/session-runtime.ts +168 -0
  79. package/src/runtime/task-backlog-runtime.ts +163 -0
  80. package/src/runtime/task-rpc.ts +150 -0
  81. package/src/store.ts +132 -50
  82. package/src/task-backlog-coordinator.ts +32 -0
  83. package/src/task-reducer.ts +152 -0
  84. package/src/task-store.ts +103 -31
  85. package/src/tools/loop-tools.ts +304 -0
  86. package/src/tools/monitor-tools.ts +145 -0
  87. package/src/tools/native-task-tools.ts +144 -0
@@ -0,0 +1,118 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export function createTaskRuntimeBridge(options) {
3
+ const { pi, isTasksAvailable, setTasksAvailable, getNativeTaskStore, onNativeTaskCreated, onNativeTasksPruned, debug, } = options;
4
+ function checkTasksVersion() {
5
+ const requestId = randomUUID();
6
+ const timer = setTimeout(() => {
7
+ unsub();
8
+ }, 5000);
9
+ const unsub = pi.events.on(`tasks:rpc:ping:reply:${requestId}`, (raw) => {
10
+ unsub();
11
+ clearTimeout(timer);
12
+ const remoteVersion = raw?.data?.version;
13
+ if (remoteVersion !== undefined)
14
+ setTasksAvailable(true);
15
+ });
16
+ pi.events.emit("tasks:rpc:ping", { requestId });
17
+ }
18
+ async function autoCreateTask(entry) {
19
+ if (!entry.autoTask)
20
+ return undefined;
21
+ if (isTasksAvailable()) {
22
+ try {
23
+ const requestId = randomUUID();
24
+ const taskId = await new Promise((resolve) => {
25
+ const timer = setTimeout(() => {
26
+ unsub();
27
+ resolve(undefined);
28
+ }, 5000);
29
+ const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw) => {
30
+ unsub();
31
+ clearTimeout(timer);
32
+ const reply = raw;
33
+ if (reply.success && reply.data)
34
+ resolve(reply.data.id);
35
+ else
36
+ resolve(undefined);
37
+ });
38
+ pi.events.emit("tasks:rpc:create", {
39
+ requestId,
40
+ subject: entry.prompt.slice(0, 80),
41
+ description: `Auto-created from loop #${entry.id}`,
42
+ metadata: { loopId: entry.id, trigger: entry.trigger },
43
+ });
44
+ });
45
+ return taskId;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ const nativeTaskStore = getNativeTaskStore();
52
+ if (!nativeTaskStore)
53
+ return undefined;
54
+ const task = nativeTaskStore.create(entry.prompt.slice(0, 80), `Auto-created from loop #${entry.id}`, { loopId: entry.id, trigger: entry.trigger });
55
+ onNativeTaskCreated?.(nativeTaskStore);
56
+ return task.id;
57
+ }
58
+ async function hasPendingTasks() {
59
+ if (isTasksAvailable()) {
60
+ try {
61
+ const requestId = randomUUID();
62
+ const count = await new Promise((resolve) => {
63
+ const timer = setTimeout(() => {
64
+ unsub();
65
+ resolve(-1);
66
+ }, 3000);
67
+ const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw) => {
68
+ unsub();
69
+ clearTimeout(timer);
70
+ const reply = raw;
71
+ resolve(reply.success && reply.data ? reply.data.pending : -1);
72
+ });
73
+ pi.events.emit("tasks:rpc:pending", { requestId });
74
+ });
75
+ return count;
76
+ }
77
+ catch {
78
+ return -1;
79
+ }
80
+ }
81
+ return getNativeTaskStore()?.pendingCount() ?? -1;
82
+ }
83
+ async function cleanDoneTasks() {
84
+ if (isTasksAvailable()) {
85
+ try {
86
+ const requestId = randomUUID();
87
+ await new Promise((resolve) => {
88
+ const timer = setTimeout(() => {
89
+ unsub();
90
+ resolve();
91
+ }, 3000);
92
+ const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
93
+ unsub();
94
+ clearTimeout(timer);
95
+ debug?.("tasks:rpc:clean — done tasks swept");
96
+ resolve();
97
+ });
98
+ pi.events.emit("tasks:rpc:clean", { requestId });
99
+ });
100
+ }
101
+ catch {
102
+ // timeout or error, ignore
103
+ }
104
+ return;
105
+ }
106
+ const nativeTaskStore = getNativeTaskStore();
107
+ if (!nativeTaskStore)
108
+ return;
109
+ nativeTaskStore.pruneCompleted();
110
+ await onNativeTasksPruned?.(nativeTaskStore);
111
+ }
112
+ return {
113
+ checkTasksVersion,
114
+ autoCreateTask,
115
+ hasPendingTasks,
116
+ cleanDoneTasks,
117
+ };
118
+ }
package/dist/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LoopEntry, LoopStatus, Trigger } from "./types.js";
1
+ import type { LoopEntry, Trigger } from "./types.js";
2
2
  export declare class LoopStore {
3
3
  private filePath;
4
4
  private lockPath;
@@ -8,6 +8,8 @@ export declare class LoopStore {
8
8
  private load;
9
9
  private save;
10
10
  private withLock;
11
+ private toReducerState;
12
+ private applyReducerEvent;
11
13
  create(trigger: Trigger, prompt: string, opts: {
12
14
  recurring: boolean;
13
15
  autoTask?: boolean;
@@ -17,11 +19,12 @@ export declare class LoopStore {
17
19
  }): LoopEntry;
18
20
  get(id: string): LoopEntry | undefined;
19
21
  list(): LoopEntry[];
20
- update(id: string, fields: {
21
- status?: LoopStatus;
22
+ pause(id: string): LoopEntry | undefined;
23
+ resume(id: string): LoopEntry | undefined;
24
+ fire(id: string): LoopEntry | undefined;
25
+ updateMetadata(id: string, fields: {
22
26
  trigger?: Trigger;
23
27
  prompt?: string;
24
- fireCount?: number;
25
28
  }): {
26
29
  entry: LoopEntry | undefined;
27
30
  changedFields: string[];
package/dist/store.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
+ import { reduceLoopState } from "./loop-reducer.js";
4
5
  const LOOPS_DIR = join(homedir(), ".pi", "loops");
5
6
  const LOCK_RETRY_MS = 50;
6
7
  const LOCK_MAX_RETRIES = 100;
7
8
  const MAX_LOOPS = 25;
8
- const MAX_EXPIRY_DAYS = 7;
9
9
  function acquireLock(lockPath) {
10
10
  for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
11
11
  try {
@@ -104,29 +104,39 @@ export class LoopStore {
104
104
  releaseLock(this.lockPath);
105
105
  }
106
106
  }
107
+ toReducerState() {
108
+ return {
109
+ nextId: this.nextId,
110
+ loopsById: Object.fromEntries(this.loops.entries()),
111
+ };
112
+ }
113
+ applyReducerEvent(event) {
114
+ const result = reduceLoopState(this.toReducerState(), event);
115
+ this.nextId = result.state.nextId;
116
+ this.loops = new Map(Object.entries(result.state.loopsById));
117
+ }
107
118
  create(trigger, prompt, opts) {
108
119
  return this.withLock(() => {
109
120
  if (this.loops.size >= MAX_LOOPS) {
110
121
  throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
111
122
  }
112
123
  const now = Date.now();
113
- const entry = {
114
- id: String(this.nextId++),
115
- prompt,
116
- trigger,
117
- status: "active",
118
- recurring: opts.recurring,
119
- autoTask: opts.autoTask,
120
- taskBacklog: opts.taskBacklog,
121
- readOnly: opts.readOnly,
122
- maxFires: opts.maxFires,
123
- fireCount: 0,
124
- createdAt: now,
125
- updatedAt: now,
126
- expiresAt: now + MAX_EXPIRY_DAYS * 24 * 60 * 60 * 1000,
127
- };
128
- this.loops.set(entry.id, entry);
129
- return entry;
124
+ this.applyReducerEvent({
125
+ type: "LOOP_CREATED",
126
+ at: now,
127
+ source: "tool",
128
+ entityType: "loop",
129
+ payload: {
130
+ prompt,
131
+ trigger,
132
+ recurring: opts.recurring,
133
+ autoTask: opts.autoTask,
134
+ taskBacklog: opts.taskBacklog,
135
+ readOnly: opts.readOnly,
136
+ maxFires: opts.maxFires,
137
+ },
138
+ });
139
+ return this.loops.get(String(this.nextId - 1));
130
140
  });
131
141
  }
132
142
  get(id) {
@@ -139,37 +149,87 @@ export class LoopStore {
139
149
  this.load();
140
150
  return Array.from(this.loops.values()).sort((a, b) => Number(a.id) - Number(b.id));
141
151
  }
142
- update(id, fields) {
152
+ pause(id) {
153
+ return this.withLock(() => {
154
+ const entry = this.loops.get(id);
155
+ if (!entry)
156
+ return undefined;
157
+ this.applyReducerEvent({
158
+ type: "LOOP_PAUSED",
159
+ at: Date.now(),
160
+ source: "tool",
161
+ entityType: "loop",
162
+ entityId: id,
163
+ payload: { id },
164
+ });
165
+ return this.loops.get(id);
166
+ });
167
+ }
168
+ resume(id) {
169
+ return this.withLock(() => {
170
+ const entry = this.loops.get(id);
171
+ if (!entry)
172
+ return undefined;
173
+ this.applyReducerEvent({
174
+ type: "LOOP_RESUMED",
175
+ at: Date.now(),
176
+ source: "tool",
177
+ entityType: "loop",
178
+ entityId: id,
179
+ payload: { id },
180
+ });
181
+ return this.loops.get(id);
182
+ });
183
+ }
184
+ fire(id) {
143
185
  return this.withLock(() => {
144
186
  const entry = this.loops.get(id);
145
187
  if (!entry)
188
+ return undefined;
189
+ this.applyReducerEvent({
190
+ type: "LOOP_FIRED",
191
+ at: Date.now(),
192
+ source: "system",
193
+ entityType: "loop",
194
+ entityId: id,
195
+ payload: { id },
196
+ });
197
+ return this.loops.get(id);
198
+ });
199
+ }
200
+ updateMetadata(id, fields) {
201
+ return this.withLock(() => {
202
+ const current = this.loops.get(id);
203
+ if (!current)
146
204
  return { entry: undefined, changedFields: [] };
147
205
  const changedFields = [];
148
- if (fields.status !== undefined) {
149
- entry.status = fields.status;
150
- changedFields.push("status");
151
- }
206
+ const now = Date.now();
152
207
  if (fields.trigger !== undefined) {
153
- entry.trigger = fields.trigger;
208
+ current.trigger = fields.trigger;
154
209
  changedFields.push("trigger");
155
210
  }
156
211
  if (fields.prompt !== undefined) {
157
- entry.prompt = fields.prompt;
212
+ current.prompt = fields.prompt;
158
213
  changedFields.push("prompt");
159
214
  }
160
- if (fields.fireCount !== undefined) {
161
- entry.fireCount = fields.fireCount;
162
- changedFields.push("fireCount");
215
+ if (changedFields.length > 0) {
216
+ current.updatedAt = now;
163
217
  }
164
- entry.updatedAt = Date.now();
165
- return { entry, changedFields };
218
+ return { entry: this.loops.get(id), changedFields };
166
219
  });
167
220
  }
168
221
  delete(id) {
169
222
  return this.withLock(() => {
170
223
  if (!this.loops.has(id))
171
224
  return false;
172
- this.loops.delete(id);
225
+ this.applyReducerEvent({
226
+ type: "LOOP_DELETED",
227
+ at: Date.now(),
228
+ source: "tool",
229
+ entityType: "loop",
230
+ entityId: id,
231
+ payload: { id },
232
+ });
173
233
  return true;
174
234
  });
175
235
  }
@@ -177,11 +237,18 @@ export class LoopStore {
177
237
  return this.withLock(() => {
178
238
  const now = Date.now();
179
239
  let count = 0;
180
- for (const [id, entry] of this.loops) {
181
- if (now >= entry.expiresAt) {
182
- this.loops.delete(id);
183
- count++;
184
- }
240
+ for (const [id, entry] of [...this.loops.entries()]) {
241
+ if (now < entry.expiresAt)
242
+ continue;
243
+ this.applyReducerEvent({
244
+ type: "LOOP_EXPIRED",
245
+ at: now,
246
+ source: "system",
247
+ entityType: "loop",
248
+ entityId: id,
249
+ payload: { id, reason: "expires_at" },
250
+ });
251
+ count++;
185
252
  }
186
253
  return count;
187
254
  });
@@ -189,27 +256,40 @@ export class LoopStore {
189
256
  expireEventLoops(sessionStartedAt) {
190
257
  return this.withLock(() => {
191
258
  let count = 0;
192
- const toDelete = [];
193
- for (const [id, entry] of this.loops) {
259
+ for (const [id, entry] of [...this.loops.entries()]) {
194
260
  if (entry.status !== "active")
195
261
  continue;
196
- if (entry.trigger.type === "event" || entry.trigger.type === "hybrid") {
197
- if (entry.createdAt < sessionStartedAt) {
198
- toDelete.push(id);
199
- count++;
200
- }
201
- }
262
+ if (entry.trigger.type !== "event" && entry.trigger.type !== "hybrid")
263
+ continue;
264
+ if (entry.createdAt >= sessionStartedAt)
265
+ continue;
266
+ this.applyReducerEvent({
267
+ type: "LOOP_EXPIRED",
268
+ at: sessionStartedAt,
269
+ source: "session",
270
+ entityType: "loop",
271
+ entityId: id,
272
+ payload: { id, reason: "resume_event_stale" },
273
+ });
274
+ count++;
202
275
  }
203
- for (const id of toDelete)
204
- this.loops.delete(id);
205
276
  return count;
206
277
  });
207
278
  }
208
279
  clearAll() {
209
280
  return this.withLock(() => {
210
- const count = this.loops.size;
211
- this.loops.clear();
212
- return count;
281
+ const ids = [...this.loops.keys()];
282
+ for (const id of ids) {
283
+ this.applyReducerEvent({
284
+ type: "LOOP_DELETED",
285
+ at: Date.now(),
286
+ source: "system",
287
+ entityType: "loop",
288
+ entityId: id,
289
+ payload: { id },
290
+ });
291
+ }
292
+ return ids.length;
213
293
  });
214
294
  }
215
295
  deleteFileIfEmpty() {
@@ -0,0 +1,12 @@
1
+ import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
2
+ export type TaskBacklogEvent = ReducerEvent<"TASK_BACKLOG_EVALUATED", {
3
+ pendingCount: number;
4
+ threshold: number;
5
+ }>;
6
+ export type TaskBacklogEffect = ReducerEffect<"ENSURE_AUTO_TASK_WORKER", {
7
+ pendingCount: number;
8
+ threshold: number;
9
+ }> | ReducerEffect<"CLEANUP_TASK_BACKLOG_LOOPS", {
10
+ pendingCount: number;
11
+ }>;
12
+ export declare function reduceTaskBacklogEvent(event: TaskBacklogEvent): TaskBacklogEffect[];
@@ -0,0 +1,22 @@
1
+ export function reduceTaskBacklogEvent(event) {
2
+ if (event.type !== "TASK_BACKLOG_EVALUATED")
3
+ return [];
4
+ const { pendingCount, threshold } = event.payload;
5
+ if (pendingCount < 0)
6
+ return [];
7
+ if (pendingCount === 0) {
8
+ return [{
9
+ type: "CLEANUP_TASK_BACKLOG_LOOPS",
10
+ entityType: "task",
11
+ payload: { pendingCount },
12
+ }];
13
+ }
14
+ if (pendingCount >= threshold) {
15
+ return [{
16
+ type: "ENSURE_AUTO_TASK_WORKER",
17
+ entityType: "task",
18
+ payload: { pendingCount, threshold },
19
+ }];
20
+ }
21
+ return [];
22
+ }
@@ -0,0 +1,66 @@
1
+ import type { TaskEntry } from "./task-types.js";
2
+ export interface TaskReducerState {
3
+ nextId: number;
4
+ tasksById: Record<string, TaskEntry>;
5
+ }
6
+ export type TaskReducerEvent = {
7
+ type: "TASK_CREATED";
8
+ at: number;
9
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
10
+ entityType?: "task";
11
+ entityId?: string;
12
+ payload: {
13
+ subject: string;
14
+ description: string;
15
+ metadata?: Record<string, unknown>;
16
+ };
17
+ } | {
18
+ type: "TASK_STARTED" | "TASK_COMPLETED" | "TASK_REOPENED" | "TASK_DELETED";
19
+ at: number;
20
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
21
+ entityType?: "task";
22
+ entityId?: string;
23
+ payload: {
24
+ id: string;
25
+ };
26
+ } | {
27
+ type: "TASK_UPDATED";
28
+ at: number;
29
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
30
+ entityType?: "task";
31
+ entityId?: string;
32
+ payload: {
33
+ id: string;
34
+ subject?: string;
35
+ description?: string;
36
+ };
37
+ } | {
38
+ type: "TASKS_PRUNED";
39
+ at: number;
40
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
41
+ entityType?: "task";
42
+ entityId?: string;
43
+ payload: {
44
+ reason: "git_commit" | "zero_pending_cleanup" | "manual";
45
+ };
46
+ };
47
+ export type TaskReducerEffect = {
48
+ type: "PERSIST_TASK";
49
+ entityType: "task";
50
+ entityId: string;
51
+ payload: {
52
+ task: TaskEntry;
53
+ };
54
+ } | {
55
+ type: "DELETE_TASK";
56
+ entityType: "task";
57
+ entityId: string;
58
+ payload: {
59
+ id: string;
60
+ };
61
+ };
62
+ export interface TaskReduceResult {
63
+ state: TaskReducerState;
64
+ effects: TaskReducerEffect[];
65
+ }
66
+ export declare function reduceTaskState(state: TaskReducerState, event: TaskReducerEvent): TaskReduceResult;
@@ -0,0 +1,76 @@
1
+ function cloneState(state) {
2
+ return {
3
+ nextId: state.nextId,
4
+ tasksById: { ...state.tasksById },
5
+ };
6
+ }
7
+ export function reduceTaskState(state, event) {
8
+ if (event.type === "TASK_CREATED") {
9
+ const next = cloneState(state);
10
+ const id = String(next.nextId++);
11
+ const task = {
12
+ id,
13
+ subject: event.payload.subject,
14
+ description: event.payload.description,
15
+ status: "pending",
16
+ createdAt: event.at,
17
+ updatedAt: event.at,
18
+ metadata: event.payload.metadata,
19
+ };
20
+ next.tasksById[id] = task;
21
+ return {
22
+ state: next,
23
+ effects: [{ type: "PERSIST_TASK", entityType: "task", entityId: id, payload: { task } }],
24
+ };
25
+ }
26
+ if (event.type === "TASKS_PRUNED") {
27
+ const next = cloneState(state);
28
+ const effects = [];
29
+ for (const [id, task] of Object.entries(next.tasksById)) {
30
+ if (task.status !== "completed")
31
+ continue;
32
+ delete next.tasksById[id];
33
+ effects.push({ type: "DELETE_TASK", entityType: "task", entityId: id, payload: { id } });
34
+ }
35
+ return { state: next, effects };
36
+ }
37
+ const id = event.payload.id;
38
+ const current = state.tasksById[id];
39
+ if (!current)
40
+ return { state, effects: [] };
41
+ if (event.type === "TASK_DELETED") {
42
+ const next = cloneState(state);
43
+ delete next.tasksById[id];
44
+ return {
45
+ state: next,
46
+ effects: [{ type: "DELETE_TASK", entityType: "task", entityId: id, payload: { id } }],
47
+ };
48
+ }
49
+ const next = cloneState(state);
50
+ const task = { ...current };
51
+ if (event.type === "TASK_STARTED") {
52
+ task.status = "in_progress";
53
+ task.updatedAt = event.at;
54
+ }
55
+ if (event.type === "TASK_COMPLETED") {
56
+ task.status = "completed";
57
+ task.updatedAt = event.at;
58
+ task.completedAt = event.at;
59
+ }
60
+ if (event.type === "TASK_REOPENED") {
61
+ task.status = "pending";
62
+ task.updatedAt = event.at;
63
+ }
64
+ if (event.type === "TASK_UPDATED") {
65
+ if (event.payload.subject !== undefined)
66
+ task.subject = event.payload.subject;
67
+ if (event.payload.description !== undefined)
68
+ task.description = event.payload.description;
69
+ task.updatedAt = event.at;
70
+ }
71
+ next.tasksById[id] = task;
72
+ return {
73
+ state: next,
74
+ effects: [{ type: "PERSIST_TASK", entityType: "task", entityId: id, payload: { task } }],
75
+ };
76
+ }
@@ -1,4 +1,4 @@
1
- import type { TaskEntry, TaskStatus } from "./task-types.js";
1
+ import type { TaskEntry } from "./task-types.js";
2
2
  export declare class TaskStore {
3
3
  private filePath;
4
4
  private lockPath;
@@ -8,15 +8,19 @@ export declare class TaskStore {
8
8
  private load;
9
9
  private save;
10
10
  private withLock;
11
+ private toReducerState;
12
+ private applyReducerEvent;
11
13
  create(subject: string, description: string, metadata?: Record<string, unknown>): TaskEntry;
12
14
  get(id: string): TaskEntry | undefined;
13
15
  list(): TaskEntry[];
14
- update(id: string, fields: {
15
- status?: TaskStatus;
16
+ start(id: string): TaskEntry | undefined;
17
+ complete(id: string): TaskEntry | undefined;
18
+ reopen(id: string): TaskEntry | undefined;
19
+ updateDetails(id: string, fields: {
16
20
  subject?: string;
17
21
  description?: string;
18
22
  }): TaskEntry | undefined;
19
23
  delete(id: string): boolean;
20
24
  pendingCount(): number;
21
- sweepCompleted(): number;
25
+ pruneCompleted(): number;
22
26
  }