@trevonistrevon/pi-loop 0.5.0 → 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.
- package/README.md +11 -10
- package/dist/commands/loop-command.d.ts +22 -0
- package/dist/commands/loop-command.js +148 -0
- package/dist/commands/tasks-command.d.ts +15 -0
- package/dist/commands/tasks-command.js +117 -0
- package/dist/goal-coordinator.d.ts +22 -0
- package/dist/goal-coordinator.js +28 -0
- package/dist/goal-reducer.d.ts +89 -0
- package/dist/goal-reducer.js +181 -0
- package/dist/goal-store.d.ts +31 -0
- package/dist/goal-store.js +298 -0
- package/dist/goal-types.d.ts +4 -0
- package/dist/index.js +125 -1212
- package/dist/runtime/monitor-ondone-runtime.d.ts +13 -0
- package/dist/runtime/monitor-ondone-runtime.js +49 -0
- package/dist/runtime/notification-runtime.d.ts +34 -0
- package/dist/runtime/notification-runtime.js +152 -0
- package/dist/runtime/scope.d.ts +8 -0
- package/dist/runtime/scope.js +33 -0
- package/dist/runtime/session-runtime.d.ts +39 -0
- package/dist/runtime/session-runtime.js +110 -0
- package/dist/runtime/task-backlog-runtime.d.ts +36 -0
- package/dist/runtime/task-backlog-runtime.js +105 -0
- package/dist/runtime/task-rpc.d.ts +19 -0
- package/dist/runtime/task-rpc.js +118 -0
- package/dist/store.d.ts +5 -4
- package/dist/store.js +50 -44
- package/dist/task-store.d.ts +6 -4
- package/dist/task-store.js +64 -47
- package/dist/tools/loop-tools.d.ts +41 -0
- package/dist/tools/loop-tools.js +241 -0
- package/dist/tools/monitor-tools.d.ts +25 -0
- package/dist/tools/monitor-tools.js +110 -0
- package/dist/tools/native-task-tools.d.ts +15 -0
- package/dist/tools/native-task-tools.js +127 -0
- package/package.json +1 -1
- package/src/commands/loop-command.ts +184 -0
- package/src/commands/tasks-command.ts +135 -0
- package/src/goal-coordinator.ts +58 -0
- package/src/goal-reducer.ts +280 -0
- package/src/goal-store.ts +315 -0
- package/src/goal-types.ts +5 -0
- package/src/index.ts +129 -1299
- package/src/runtime/monitor-ondone-runtime.ts +75 -0
- package/src/runtime/notification-runtime.ts +212 -0
- package/src/runtime/scope.ts +37 -0
- package/src/runtime/session-runtime.ts +168 -0
- package/src/runtime/task-backlog-runtime.ts +163 -0
- package/src/runtime/task-rpc.ts +150 -0
- package/src/store.ts +51 -45
- package/src/task-store.ts +61 -46
- package/src/tools/loop-tools.ts +304 -0
- package/src/tools/monitor-tools.ts +145 -0
- package/src/tools/native-task-tools.ts +144 -0
|
@@ -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
|
+
}
|