@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,280 @@
1
+ import type {
2
+ GoalCriteria,
3
+ GoalEntry,
4
+ GoalProgressSnapshot,
5
+ GoalReducerState,
6
+ GoalScope,
7
+ GoalVerificationState,
8
+ } from "./goal-types.js";
9
+
10
+ export type GoalReducerEvent =
11
+ | {
12
+ type: "GOAL_CREATED";
13
+ at: number;
14
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
15
+ entityType?: "goal";
16
+ entityId?: string;
17
+ payload: {
18
+ title: string;
19
+ description: string;
20
+ scope: GoalScope;
21
+ criteria: GoalCriteria;
22
+ metadata?: Record<string, unknown>;
23
+ };
24
+ }
25
+ | {
26
+ type: "GOAL_ACTIVATED" | "GOAL_VERIFICATION_STARTED" | "GOAL_UNBLOCKED";
27
+ at: number;
28
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
29
+ entityType?: "goal";
30
+ entityId?: string;
31
+ payload: { id: string };
32
+ }
33
+ | {
34
+ type: "GOAL_PROGRESS_RECORDED";
35
+ at: number;
36
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
37
+ entityType?: "goal";
38
+ entityId?: string;
39
+ payload: { id: string; progress: GoalProgressSnapshot };
40
+ }
41
+ | {
42
+ type: "GOAL_VERIFICATION_PASSED" | "GOAL_VERIFICATION_FAILED" | "GOAL_BLOCKED" | "GOAL_FAILED";
43
+ at: number;
44
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
45
+ entityType?: "goal";
46
+ entityId?: string;
47
+ payload: { id: string; reason: string; progress?: GoalProgressSnapshot };
48
+ }
49
+ | {
50
+ type: "GOAL_SATISFIED" | "GOAL_ARCHIVED";
51
+ at: number;
52
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
53
+ entityType?: "goal";
54
+ entityId?: string;
55
+ payload: { id: string; reason?: string };
56
+ }
57
+ | {
58
+ type: "GOAL_UPDATED";
59
+ at: number;
60
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
61
+ entityType?: "goal";
62
+ entityId?: string;
63
+ payload: {
64
+ id: string;
65
+ title?: string;
66
+ description?: string;
67
+ scope?: GoalScope;
68
+ criteria?: GoalCriteria;
69
+ metadata?: Record<string, unknown>;
70
+ };
71
+ };
72
+
73
+ export type GoalReducerEffect =
74
+ | {
75
+ type: "PERSIST_GOAL";
76
+ entityType: "goal";
77
+ entityId: string;
78
+ payload: { goal: GoalEntry };
79
+ }
80
+ | {
81
+ type: "REQUEST_GOAL_VERIFICATION";
82
+ entityType: "goal";
83
+ entityId: string;
84
+ payload: { id: string };
85
+ };
86
+
87
+ export interface GoalReduceResult {
88
+ state: GoalReducerState;
89
+ effects: GoalReducerEffect[];
90
+ }
91
+
92
+ function cloneState(state: GoalReducerState): GoalReducerState {
93
+ return {
94
+ nextId: state.nextId,
95
+ goalsById: { ...state.goalsById },
96
+ };
97
+ }
98
+
99
+ function emptyProgress(): GoalProgressSnapshot {
100
+ return {
101
+ totalTasks: 0,
102
+ pendingTasks: 0,
103
+ inProgressTasks: 0,
104
+ completedTasks: 0,
105
+ activeLoops: 0,
106
+ pausedLoops: 0,
107
+ runningMonitors: 0,
108
+ completedMonitors: 0,
109
+ erroredMonitors: 0,
110
+ stoppedMonitors: 0,
111
+ };
112
+ }
113
+
114
+ function emptyVerification(): GoalVerificationState {
115
+ return {
116
+ attempts: 0,
117
+ passes: 0,
118
+ failures: 0,
119
+ };
120
+ }
121
+
122
+ function persist(goal: GoalEntry): GoalReducerEffect {
123
+ return {
124
+ type: "PERSIST_GOAL",
125
+ entityType: "goal",
126
+ entityId: goal.id,
127
+ payload: { goal },
128
+ };
129
+ }
130
+
131
+ function isTerminal(goal: GoalEntry): boolean {
132
+ return goal.status === "satisfied" || goal.status === "failed" || goal.status === "archived";
133
+ }
134
+
135
+ export function reduceGoalState(state: GoalReducerState, event: GoalReducerEvent): GoalReduceResult {
136
+ if (event.type === "GOAL_CREATED") {
137
+ const next = cloneState(state);
138
+ const id = String(next.nextId++);
139
+ const goal: GoalEntry = {
140
+ id,
141
+ title: event.payload.title,
142
+ description: event.payload.description,
143
+ status: "pending",
144
+ verificationStatus: "unknown",
145
+ createdAt: event.at,
146
+ updatedAt: event.at,
147
+ scope: event.payload.scope,
148
+ criteria: event.payload.criteria,
149
+ progress: emptyProgress(),
150
+ verification: emptyVerification(),
151
+ metadata: event.payload.metadata,
152
+ };
153
+ next.goalsById[id] = goal;
154
+ return {
155
+ state: next,
156
+ effects: [persist(goal)],
157
+ };
158
+ }
159
+
160
+ const id = event.payload.id;
161
+ const current = state.goalsById[id];
162
+ if (!current) return { state, effects: [] };
163
+ if (isTerminal(current) && event.type !== "GOAL_ARCHIVED") return { state, effects: [] };
164
+
165
+ const next = cloneState(state);
166
+ const goal: GoalEntry = {
167
+ ...current,
168
+ progress: { ...current.progress },
169
+ verification: { ...current.verification },
170
+ };
171
+
172
+ let extraEffects: GoalReducerEffect[] = [];
173
+
174
+ if (event.type === "GOAL_ACTIVATED") {
175
+ goal.status = "active";
176
+ goal.activatedAt ??= event.at;
177
+ goal.updatedAt = event.at;
178
+ extraEffects = [{
179
+ type: "REQUEST_GOAL_VERIFICATION",
180
+ entityType: "goal",
181
+ entityId: id,
182
+ payload: { id },
183
+ }];
184
+ }
185
+
186
+ if (event.type === "GOAL_PROGRESS_RECORDED") {
187
+ goal.progress = event.payload.progress;
188
+ goal.updatedAt = event.at;
189
+ }
190
+
191
+ if (event.type === "GOAL_VERIFICATION_STARTED") {
192
+ goal.verificationStatus = "checking";
193
+ goal.verification.attempts += 1;
194
+ goal.verification.lastCheckedAt = event.at;
195
+ goal.updatedAt = event.at;
196
+ }
197
+
198
+ if (event.type === "GOAL_VERIFICATION_PASSED") {
199
+ if (event.payload.progress) goal.progress = event.payload.progress;
200
+ goal.status = "satisfied";
201
+ goal.verificationStatus = "verified";
202
+ goal.verification.passes += 1;
203
+ goal.verification.lastPassedAt = event.at;
204
+ goal.verification.lastCheckedAt = event.at;
205
+ goal.verification.lastReason = event.payload.reason;
206
+ goal.resolvedAt = event.at;
207
+ goal.updatedAt = event.at;
208
+ }
209
+
210
+ if (event.type === "GOAL_VERIFICATION_FAILED") {
211
+ if (event.payload.progress) goal.progress = event.payload.progress;
212
+ goal.verificationStatus = "unverified";
213
+ goal.verification.failures += 1;
214
+ goal.verification.lastFailedAt = event.at;
215
+ goal.verification.lastCheckedAt = event.at;
216
+ goal.verification.lastReason = event.payload.reason;
217
+ if (goal.status === "pending") goal.status = "active";
218
+ goal.updatedAt = event.at;
219
+ }
220
+
221
+ if (event.type === "GOAL_BLOCKED") {
222
+ if (event.payload.progress) goal.progress = event.payload.progress;
223
+ goal.status = "blocked";
224
+ goal.verificationStatus = "inconclusive";
225
+ goal.verification.failures += 1;
226
+ goal.verification.lastFailedAt = event.at;
227
+ goal.verification.lastCheckedAt = event.at;
228
+ goal.verification.lastReason = event.payload.reason;
229
+ goal.updatedAt = event.at;
230
+ }
231
+
232
+ if (event.type === "GOAL_UNBLOCKED") {
233
+ goal.status = "active";
234
+ goal.updatedAt = event.at;
235
+ }
236
+
237
+ if (event.type === "GOAL_SATISFIED") {
238
+ goal.status = "satisfied";
239
+ goal.verificationStatus = "verified";
240
+ goal.verification.lastReason = event.payload.reason ?? goal.verification.lastReason;
241
+ goal.resolvedAt = event.at;
242
+ goal.updatedAt = event.at;
243
+ }
244
+
245
+ if (event.type === "GOAL_FAILED") {
246
+ if (event.payload.progress) goal.progress = event.payload.progress;
247
+ goal.status = "failed";
248
+ goal.verificationStatus = "unverified";
249
+ goal.verification.failures += 1;
250
+ goal.verification.lastFailedAt = event.at;
251
+ goal.verification.lastCheckedAt = event.at;
252
+ goal.verification.lastReason = event.payload.reason;
253
+ goal.resolvedAt = event.at;
254
+ goal.updatedAt = event.at;
255
+ }
256
+
257
+ if (event.type === "GOAL_ARCHIVED") {
258
+ goal.status = "archived";
259
+ goal.resolvedAt = event.at;
260
+ goal.updatedAt = event.at;
261
+ if (event.payload.reason !== undefined) {
262
+ goal.verification.lastReason = event.payload.reason;
263
+ }
264
+ }
265
+
266
+ if (event.type === "GOAL_UPDATED") {
267
+ if (event.payload.title !== undefined) goal.title = event.payload.title;
268
+ if (event.payload.description !== undefined) goal.description = event.payload.description;
269
+ if (event.payload.scope !== undefined) goal.scope = event.payload.scope;
270
+ if (event.payload.criteria !== undefined) goal.criteria = event.payload.criteria;
271
+ if (event.payload.metadata !== undefined) goal.metadata = event.payload.metadata;
272
+ goal.updatedAt = event.at;
273
+ }
274
+
275
+ next.goalsById[id] = goal;
276
+ return {
277
+ state: next,
278
+ effects: [persist(goal), ...extraEffects],
279
+ };
280
+ }
@@ -0,0 +1,315 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ import { type GoalReducerEvent, reduceGoalState } from "./goal-reducer.js";
5
+ import type {
6
+ GoalCriteria,
7
+ GoalEntry,
8
+ GoalProgressSnapshot,
9
+ GoalReducerState,
10
+ GoalScope,
11
+ GoalStoreData,
12
+ } from "./goal-types.js";
13
+
14
+ const GOALS_DIR = join(homedir(), ".pi", "goals");
15
+ const LOCK_RETRY_MS = 50;
16
+ const LOCK_MAX_RETRIES = 100;
17
+ const MAX_GOALS = 200;
18
+
19
+ function acquireLock(lockPath: string): void {
20
+ for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
21
+ try {
22
+ writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
23
+ return;
24
+ } catch (e: any) {
25
+ if (e.code === "EEXIST") {
26
+ try {
27
+ const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
28
+ if (!pid || !isProcessRunning(pid)) {
29
+ try { unlinkSync(lockPath); } catch { /* ignore */ }
30
+ continue;
31
+ }
32
+ } catch { /* ignore read errors */ }
33
+ const start = Date.now();
34
+ while (Date.now() - start < LOCK_RETRY_MS) { /* busy wait */ }
35
+ continue;
36
+ }
37
+ throw e;
38
+ }
39
+ }
40
+ throw new Error(`Failed to acquire lock: ${lockPath}`);
41
+ }
42
+
43
+ function releaseLock(lockPath: string): void {
44
+ try { unlinkSync(lockPath); } catch { /* ignore */ }
45
+ }
46
+
47
+ function isProcessRunning(pid: number): boolean {
48
+ try { process.kill(pid, 0); return true; } catch { return false; }
49
+ }
50
+
51
+ export class GoalStore {
52
+ private filePath: string | undefined;
53
+ private lockPath: string | undefined;
54
+
55
+ private nextId = 1;
56
+ private goals = new Map<string, GoalEntry>();
57
+
58
+ constructor(listIdOrPath?: string) {
59
+ if (!listIdOrPath) return;
60
+ const isAbsPath = isAbsolute(listIdOrPath);
61
+ const filePath = isAbsPath ? listIdOrPath : join(GOALS_DIR, `${listIdOrPath}.json`);
62
+ mkdirSync(dirname(filePath), { recursive: true });
63
+ this.filePath = filePath;
64
+ this.lockPath = filePath + ".lock";
65
+ this.load();
66
+ }
67
+
68
+ private load(): void {
69
+ if (!this.filePath) return;
70
+ if (!existsSync(this.filePath)) return;
71
+ try {
72
+ const data: GoalStoreData = JSON.parse(readFileSync(this.filePath, "utf-8"));
73
+ this.nextId = data.nextId;
74
+ this.goals.clear();
75
+ for (const goal of data.goals) {
76
+ this.goals.set(goal.id, goal);
77
+ }
78
+ } catch { /* corrupt file — start fresh */ }
79
+ }
80
+
81
+ private save(): void {
82
+ if (!this.filePath) return;
83
+ const data: GoalStoreData = {
84
+ nextId: this.nextId,
85
+ goals: Array.from(this.goals.values()),
86
+ };
87
+ const tmpPath = this.filePath + ".tmp";
88
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2));
89
+ renameSync(tmpPath, this.filePath);
90
+ }
91
+
92
+ private withLock<T>(fn: () => T): T {
93
+ if (!this.lockPath) return fn();
94
+ acquireLock(this.lockPath);
95
+ try {
96
+ this.load();
97
+ const result = fn();
98
+ this.save();
99
+ return result;
100
+ } finally {
101
+ releaseLock(this.lockPath);
102
+ }
103
+ }
104
+
105
+ private toReducerState(): GoalReducerState {
106
+ return {
107
+ nextId: this.nextId,
108
+ goalsById: Object.fromEntries(this.goals.entries()),
109
+ };
110
+ }
111
+
112
+ private applyReducerEvent(event: GoalReducerEvent): void {
113
+ const result = reduceGoalState(this.toReducerState(), event);
114
+ this.nextId = result.state.nextId;
115
+ this.goals = new Map(Object.entries(result.state.goalsById));
116
+ }
117
+
118
+ create(
119
+ title: string,
120
+ description: string,
121
+ scope: GoalScope,
122
+ criteria: GoalCriteria,
123
+ metadata?: Record<string, unknown>,
124
+ ): GoalEntry {
125
+ return this.withLock(() => {
126
+ if (this.goals.size >= MAX_GOALS) {
127
+ throw new Error(`Maximum of ${MAX_GOALS} goals reached. Archive some before creating new ones.`);
128
+ }
129
+ this.applyReducerEvent({
130
+ type: "GOAL_CREATED",
131
+ at: Date.now(),
132
+ source: "tool",
133
+ entityType: "goal",
134
+ payload: {
135
+ title,
136
+ description,
137
+ scope,
138
+ criteria,
139
+ metadata,
140
+ },
141
+ });
142
+ return this.goals.get(String(this.nextId - 1))!;
143
+ });
144
+ }
145
+
146
+ get(id: string): GoalEntry | undefined {
147
+ if (this.filePath) this.load();
148
+ return this.goals.get(id);
149
+ }
150
+
151
+ list(): GoalEntry[] {
152
+ if (this.filePath) this.load();
153
+ return Array.from(this.goals.values()).sort((a, b) => Number(a.id) - Number(b.id));
154
+ }
155
+
156
+ activate(id: string): GoalEntry | undefined {
157
+ return this.withLock(() => {
158
+ if (!this.goals.has(id)) return undefined;
159
+ this.applyReducerEvent({
160
+ type: "GOAL_ACTIVATED",
161
+ at: Date.now(),
162
+ source: "tool",
163
+ entityType: "goal",
164
+ entityId: id,
165
+ payload: { id },
166
+ });
167
+ return this.goals.get(id);
168
+ });
169
+ }
170
+
171
+ recordProgress(id: string, progress: GoalProgressSnapshot): GoalEntry | undefined {
172
+ return this.withLock(() => {
173
+ if (!this.goals.has(id)) return undefined;
174
+ this.applyReducerEvent({
175
+ type: "GOAL_PROGRESS_RECORDED",
176
+ at: Date.now(),
177
+ source: "coordinator",
178
+ entityType: "goal",
179
+ entityId: id,
180
+ payload: { id, progress },
181
+ });
182
+ return this.goals.get(id);
183
+ });
184
+ }
185
+
186
+ markVerificationStarted(id: string): GoalEntry | undefined {
187
+ return this.withLock(() => {
188
+ if (!this.goals.has(id)) return undefined;
189
+ this.applyReducerEvent({
190
+ type: "GOAL_VERIFICATION_STARTED",
191
+ at: Date.now(),
192
+ source: "coordinator",
193
+ entityType: "goal",
194
+ entityId: id,
195
+ payload: { id },
196
+ });
197
+ return this.goals.get(id);
198
+ });
199
+ }
200
+
201
+ markVerified(id: string, reason: string, progress?: GoalProgressSnapshot): GoalEntry | undefined {
202
+ return this.withLock(() => {
203
+ if (!this.goals.has(id)) return undefined;
204
+ this.applyReducerEvent({
205
+ type: "GOAL_VERIFICATION_PASSED",
206
+ at: Date.now(),
207
+ source: "coordinator",
208
+ entityType: "goal",
209
+ entityId: id,
210
+ payload: { id, reason, progress },
211
+ });
212
+ return this.goals.get(id);
213
+ });
214
+ }
215
+
216
+ markFailed(id: string, reason: string, progress?: GoalProgressSnapshot): GoalEntry | undefined {
217
+ return this.withLock(() => {
218
+ if (!this.goals.has(id)) return undefined;
219
+ this.applyReducerEvent({
220
+ type: "GOAL_FAILED",
221
+ at: Date.now(),
222
+ source: "coordinator",
223
+ entityType: "goal",
224
+ entityId: id,
225
+ payload: { id, reason, progress },
226
+ });
227
+ return this.goals.get(id);
228
+ });
229
+ }
230
+
231
+ markBlocked(id: string, reason: string, progress?: GoalProgressSnapshot): GoalEntry | undefined {
232
+ return this.withLock(() => {
233
+ if (!this.goals.has(id)) return undefined;
234
+ this.applyReducerEvent({
235
+ type: "GOAL_BLOCKED",
236
+ at: Date.now(),
237
+ source: "coordinator",
238
+ entityType: "goal",
239
+ entityId: id,
240
+ payload: { id, reason, progress },
241
+ });
242
+ return this.goals.get(id);
243
+ });
244
+ }
245
+
246
+ unblock(id: string): GoalEntry | undefined {
247
+ return this.withLock(() => {
248
+ if (!this.goals.has(id)) return undefined;
249
+ this.applyReducerEvent({
250
+ type: "GOAL_UNBLOCKED",
251
+ at: Date.now(),
252
+ source: "coordinator",
253
+ entityType: "goal",
254
+ entityId: id,
255
+ payload: { id },
256
+ });
257
+ return this.goals.get(id);
258
+ });
259
+ }
260
+
261
+ updateDetails(
262
+ id: string,
263
+ fields: {
264
+ title?: string;
265
+ description?: string;
266
+ scope?: GoalScope;
267
+ criteria?: GoalCriteria;
268
+ metadata?: Record<string, unknown>;
269
+ },
270
+ ): GoalEntry | undefined {
271
+ return this.withLock(() => {
272
+ if (!this.goals.has(id)) return undefined;
273
+ if (
274
+ fields.title === undefined
275
+ && fields.description === undefined
276
+ && fields.scope === undefined
277
+ && fields.criteria === undefined
278
+ && fields.metadata === undefined
279
+ ) {
280
+ return this.goals.get(id);
281
+ }
282
+ this.applyReducerEvent({
283
+ type: "GOAL_UPDATED",
284
+ at: Date.now(),
285
+ source: "tool",
286
+ entityType: "goal",
287
+ entityId: id,
288
+ payload: {
289
+ id,
290
+ title: fields.title,
291
+ description: fields.description,
292
+ scope: fields.scope,
293
+ criteria: fields.criteria,
294
+ metadata: fields.metadata,
295
+ },
296
+ });
297
+ return this.goals.get(id);
298
+ });
299
+ }
300
+
301
+ archive(id: string, reason?: string): GoalEntry | undefined {
302
+ return this.withLock(() => {
303
+ if (!this.goals.has(id)) return undefined;
304
+ this.applyReducerEvent({
305
+ type: "GOAL_ARCHIVED",
306
+ at: Date.now(),
307
+ source: "tool",
308
+ entityType: "goal",
309
+ entityId: id,
310
+ payload: { id, reason },
311
+ });
312
+ return this.goals.get(id);
313
+ });
314
+ }
315
+ }
@@ -0,0 +1,104 @@
1
+ export type GoalStatus =
2
+ | "pending"
3
+ | "active"
4
+ | "satisfied"
5
+ | "blocked"
6
+ | "failed"
7
+ | "archived";
8
+
9
+ export type GoalVerificationStatus =
10
+ | "unknown"
11
+ | "checking"
12
+ | "verified"
13
+ | "unverified"
14
+ | "inconclusive";
15
+
16
+ export interface GoalScope {
17
+ taskIds?: string[];
18
+ loopIds?: string[];
19
+ monitorIds?: string[];
20
+ tags?: string[];
21
+ subjectPrefixes?: string[];
22
+ includeFutureMatchingTasks?: boolean;
23
+ includeFutureMatchingLoops?: boolean;
24
+ includeFutureMatchingMonitors?: boolean;
25
+ }
26
+
27
+ export interface GoalSuccessCriteria {
28
+ minCompletedTasks?: number;
29
+ requiredTaskIds?: string[];
30
+ requiredMonitorIdsCompleted?: string[];
31
+ requiredLoopIdsPresent?: string[];
32
+ requireNoPendingTasksInScope?: boolean;
33
+ requireLatestVerificationPass?: boolean;
34
+ }
35
+
36
+ export interface GoalFailureCriteria {
37
+ anyMonitorIdsErrored?: string[];
38
+ maxVerificationFailures?: number;
39
+ failIfTaskIdsDeleted?: string[];
40
+ }
41
+
42
+ export interface GoalBlockedCriteria {
43
+ blockedIfAllTasksCompletedButVerificationFails?: boolean;
44
+ blockedIfNoScopedProgressSinceMs?: number;
45
+ blockedIfRequiredLoopMissing?: boolean;
46
+ }
47
+
48
+ export interface GoalCriteria {
49
+ success: GoalSuccessCriteria;
50
+ failure?: GoalFailureCriteria;
51
+ blocked?: GoalBlockedCriteria;
52
+ }
53
+
54
+ export interface GoalProgressSnapshot {
55
+ totalTasks: number;
56
+ pendingTasks: number;
57
+ inProgressTasks: number;
58
+ completedTasks: number;
59
+ activeLoops: number;
60
+ pausedLoops: number;
61
+ runningMonitors: number;
62
+ completedMonitors: number;
63
+ erroredMonitors: number;
64
+ stoppedMonitors: number;
65
+ lastProgressAt?: number;
66
+ }
67
+
68
+ export interface GoalVerificationState {
69
+ attempts: number;
70
+ passes: number;
71
+ failures: number;
72
+ lastCheckedAt?: number;
73
+ lastPassedAt?: number;
74
+ lastFailedAt?: number;
75
+ lastReason?: string;
76
+ nextCheckAfter?: number;
77
+ }
78
+
79
+ export interface GoalEntry {
80
+ id: string;
81
+ title: string;
82
+ description: string;
83
+ status: GoalStatus;
84
+ verificationStatus: GoalVerificationStatus;
85
+ createdAt: number;
86
+ updatedAt: number;
87
+ activatedAt?: number;
88
+ resolvedAt?: number;
89
+ scope: GoalScope;
90
+ criteria: GoalCriteria;
91
+ progress: GoalProgressSnapshot;
92
+ verification: GoalVerificationState;
93
+ metadata?: Record<string, unknown>;
94
+ }
95
+
96
+ export interface GoalReducerState {
97
+ nextId: number;
98
+ goalsById: Record<string, GoalEntry>;
99
+ }
100
+
101
+ export interface GoalStoreData {
102
+ nextId: number;
103
+ goals: GoalEntry[];
104
+ }