@revealui/harnesses 0.1.6 → 0.1.8
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/LICENSE +104 -17
- package/dist/{chunk-JG6CAG4A.js → chunk-4F4ANKIZ.js} +3 -2
- package/dist/chunk-4F4ANKIZ.js.map +1 -0
- package/dist/chunk-6E2BKO6U.js +2040 -0
- package/dist/chunk-6E2BKO6U.js.map +1 -0
- package/dist/chunk-DGQ5OB6L.js +380 -0
- package/dist/chunk-DGQ5OB6L.js.map +1 -0
- package/dist/{chunk-XXEKWC6F.js → chunk-PROC6EJC.js} +10 -10
- package/dist/chunk-PROC6EJC.js.map +1 -0
- package/dist/cli.js +43 -7
- package/dist/cli.js.map +1 -1
- package/dist/content/index.d.ts +8 -8
- package/dist/content/index.js +1 -1
- package/dist/index.d.ts +259 -28
- package/dist/index.js +9 -3
- package/dist/storage/index.d.ts +170 -0
- package/dist/storage/index.js +10 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/workboard/index.js +1 -1
- package/package.json +10 -4
- package/LICENSE.commercial +0 -111
- package/dist/chunk-JG6CAG4A.js.map +0 -1
- package/dist/chunk-XLIKSLM3.js +0 -1105
- package/dist/chunk-XLIKSLM3.js.map +0 -1
- package/dist/chunk-XXEKWC6F.js.map +0 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// src/storage/schema.ts
|
|
2
|
+
var SCHEMA_SQL = `
|
|
3
|
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
env TEXT NOT NULL DEFAULT '',
|
|
6
|
+
task TEXT NOT NULL DEFAULT '(starting)',
|
|
7
|
+
files TEXT NOT NULL DEFAULT '',
|
|
8
|
+
pid INTEGER,
|
|
9
|
+
started_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
10
|
+
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
11
|
+
ended_at TIMESTAMP,
|
|
12
|
+
exit_summary TEXT
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS agent_messages (
|
|
16
|
+
id SERIAL PRIMARY KEY,
|
|
17
|
+
from_agent TEXT NOT NULL,
|
|
18
|
+
to_agent TEXT NOT NULL,
|
|
19
|
+
subject TEXT NOT NULL DEFAULT '',
|
|
20
|
+
body TEXT NOT NULL DEFAULT '',
|
|
21
|
+
read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
22
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_messages_to_unread
|
|
26
|
+
ON agent_messages (to_agent, read) WHERE read = FALSE;
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS file_reservations (
|
|
29
|
+
file_path TEXT PRIMARY KEY,
|
|
30
|
+
agent_id TEXT NOT NULL,
|
|
31
|
+
reserved_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
32
|
+
expires_at TIMESTAMP NOT NULL,
|
|
33
|
+
reason TEXT NOT NULL DEFAULT ''
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_agent
|
|
37
|
+
ON file_reservations (agent_id);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
description TEXT NOT NULL DEFAULT '',
|
|
42
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
43
|
+
owner TEXT,
|
|
44
|
+
claimed_at TIMESTAMP,
|
|
45
|
+
completed_at TIMESTAMP,
|
|
46
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status
|
|
50
|
+
ON tasks (status);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_owner
|
|
53
|
+
ON tasks (owner) WHERE owner IS NOT NULL;
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
56
|
+
id SERIAL PRIMARY KEY,
|
|
57
|
+
agent_id TEXT NOT NULL,
|
|
58
|
+
event_type TEXT NOT NULL,
|
|
59
|
+
payload JSONB NOT NULL DEFAULT '{}',
|
|
60
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_events_agent
|
|
64
|
+
ON events (agent_id, created_at DESC);
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
// src/storage/daemon-store.ts
|
|
68
|
+
var DaemonStore = class {
|
|
69
|
+
db = null;
|
|
70
|
+
dataDir;
|
|
71
|
+
constructor(config) {
|
|
72
|
+
this.dataDir = config.dataDir;
|
|
73
|
+
}
|
|
74
|
+
/** Initialize PGlite and create tables. */
|
|
75
|
+
async init() {
|
|
76
|
+
const { PGlite: PGliteClass } = await import("@electric-sql/pglite");
|
|
77
|
+
this.db = new PGliteClass(this.dataDir);
|
|
78
|
+
await this.db.exec(SCHEMA_SQL);
|
|
79
|
+
}
|
|
80
|
+
/** Shut down the database. */
|
|
81
|
+
async close() {
|
|
82
|
+
if (this.db) {
|
|
83
|
+
await this.db.close();
|
|
84
|
+
this.db = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
getDb() {
|
|
88
|
+
if (!this.db) throw new Error("DaemonStore not initialized \u2014 call init() first");
|
|
89
|
+
return this.db;
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Sessions
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/** Register or update an agent session. */
|
|
95
|
+
async registerSession(session) {
|
|
96
|
+
const db = this.getDb();
|
|
97
|
+
const result = await db.query(
|
|
98
|
+
`INSERT INTO agent_sessions (id, env, task, pid)
|
|
99
|
+
VALUES ($1, $2, $3, $4)
|
|
100
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
101
|
+
env = EXCLUDED.env,
|
|
102
|
+
task = COALESCE(EXCLUDED.task, agent_sessions.task),
|
|
103
|
+
pid = COALESCE(EXCLUDED.pid, agent_sessions.pid),
|
|
104
|
+
updated_at = NOW(),
|
|
105
|
+
ended_at = NULL
|
|
106
|
+
RETURNING *`,
|
|
107
|
+
[session.id, session.env, session.task ?? "(starting)", session.pid ?? null]
|
|
108
|
+
);
|
|
109
|
+
return result.rows[0];
|
|
110
|
+
}
|
|
111
|
+
/** Update a session's task and/or files. */
|
|
112
|
+
async updateSession(id, updates) {
|
|
113
|
+
const db = this.getDb();
|
|
114
|
+
const sets = ["updated_at = NOW()"];
|
|
115
|
+
const params = [];
|
|
116
|
+
let paramIdx = 1;
|
|
117
|
+
if (updates.task !== void 0) {
|
|
118
|
+
sets.push(`task = $${paramIdx++}`);
|
|
119
|
+
params.push(updates.task);
|
|
120
|
+
}
|
|
121
|
+
if (updates.files !== void 0) {
|
|
122
|
+
sets.push(`files = $${paramIdx++}`);
|
|
123
|
+
params.push(updates.files);
|
|
124
|
+
}
|
|
125
|
+
params.push(id);
|
|
126
|
+
const result = await db.query(
|
|
127
|
+
`UPDATE agent_sessions SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
|
|
128
|
+
params
|
|
129
|
+
);
|
|
130
|
+
return result.rows[0] ?? null;
|
|
131
|
+
}
|
|
132
|
+
/** End a session (mark ended_at, record exit summary). */
|
|
133
|
+
async endSession(id, exitSummary) {
|
|
134
|
+
const db = this.getDb();
|
|
135
|
+
await db.query(
|
|
136
|
+
`UPDATE agent_sessions SET ended_at = NOW(), exit_summary = $2, updated_at = NOW()
|
|
137
|
+
WHERE id = $1`,
|
|
138
|
+
[id, exitSummary ?? null]
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
/** Get all active sessions (ended_at IS NULL). */
|
|
142
|
+
async getActiveSessions() {
|
|
143
|
+
const db = this.getDb();
|
|
144
|
+
const result = await db.query(
|
|
145
|
+
"SELECT * FROM agent_sessions WHERE ended_at IS NULL ORDER BY started_at"
|
|
146
|
+
);
|
|
147
|
+
return result.rows;
|
|
148
|
+
}
|
|
149
|
+
/** Get session history for an agent (most recent first). */
|
|
150
|
+
async getSessionHistory(agentId, limit) {
|
|
151
|
+
const db = this.getDb();
|
|
152
|
+
const result = await db.query(
|
|
153
|
+
"SELECT * FROM agent_sessions WHERE id = $1 ORDER BY started_at DESC LIMIT $2",
|
|
154
|
+
[agentId, limit]
|
|
155
|
+
);
|
|
156
|
+
return result.rows;
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Messages
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
/** Send a message from one agent to another. */
|
|
162
|
+
async sendMessage(msg) {
|
|
163
|
+
const db = this.getDb();
|
|
164
|
+
const result = await db.query(
|
|
165
|
+
`INSERT INTO agent_messages (from_agent, to_agent, subject, body)
|
|
166
|
+
VALUES ($1, $2, $3, $4)
|
|
167
|
+
RETURNING *`,
|
|
168
|
+
[msg.fromAgent, msg.toAgent, msg.subject, msg.body ?? ""]
|
|
169
|
+
);
|
|
170
|
+
return result.rows[0];
|
|
171
|
+
}
|
|
172
|
+
/** Broadcast a message to all active agents (except sender). */
|
|
173
|
+
async broadcastMessage(msg) {
|
|
174
|
+
const active = await this.getActiveSessions();
|
|
175
|
+
let sent = 0;
|
|
176
|
+
for (const session of active) {
|
|
177
|
+
if (session.id === msg.fromAgent) continue;
|
|
178
|
+
await this.sendMessage({
|
|
179
|
+
fromAgent: msg.fromAgent,
|
|
180
|
+
toAgent: session.id,
|
|
181
|
+
subject: msg.subject,
|
|
182
|
+
body: msg.body
|
|
183
|
+
});
|
|
184
|
+
sent++;
|
|
185
|
+
}
|
|
186
|
+
return sent;
|
|
187
|
+
}
|
|
188
|
+
/** Get unread messages for an agent. */
|
|
189
|
+
async getInbox(agentId, unreadOnly) {
|
|
190
|
+
const db = this.getDb();
|
|
191
|
+
const whereClause = unreadOnly ? "WHERE to_agent = $1 AND read = FALSE" : "WHERE to_agent = $1";
|
|
192
|
+
const result = await db.query(
|
|
193
|
+
`SELECT * FROM agent_messages ${whereClause} ORDER BY created_at DESC LIMIT 50`,
|
|
194
|
+
[agentId]
|
|
195
|
+
);
|
|
196
|
+
return result.rows;
|
|
197
|
+
}
|
|
198
|
+
/** Mark messages as read. */
|
|
199
|
+
async markRead(messageIds) {
|
|
200
|
+
if (messageIds.length === 0) return;
|
|
201
|
+
const db = this.getDb();
|
|
202
|
+
const placeholders = messageIds.map((_, i) => `$${i + 1}`).join(", ");
|
|
203
|
+
await db.query(
|
|
204
|
+
`UPDATE agent_messages SET read = TRUE WHERE id IN (${placeholders})`,
|
|
205
|
+
messageIds
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// File Reservations
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
/** Reserve a file for an agent (CAS: fails if already reserved by another). */
|
|
212
|
+
async reserveFile(reservation) {
|
|
213
|
+
const db = this.getDb();
|
|
214
|
+
await db.query("DELETE FROM file_reservations WHERE expires_at < NOW()");
|
|
215
|
+
const result = await db.query(
|
|
216
|
+
`INSERT INTO file_reservations (file_path, agent_id, expires_at, reason)
|
|
217
|
+
VALUES ($1, $2, NOW() + ($3 || ' seconds')::INTERVAL, $4)
|
|
218
|
+
ON CONFLICT (file_path) DO UPDATE SET
|
|
219
|
+
agent_id = EXCLUDED.agent_id,
|
|
220
|
+
reserved_at = NOW(),
|
|
221
|
+
expires_at = EXCLUDED.expires_at,
|
|
222
|
+
reason = EXCLUDED.reason
|
|
223
|
+
WHERE file_reservations.agent_id = $2 OR file_reservations.expires_at < NOW()
|
|
224
|
+
RETURNING *`,
|
|
225
|
+
[
|
|
226
|
+
reservation.filePath,
|
|
227
|
+
reservation.agentId,
|
|
228
|
+
String(reservation.ttlSeconds),
|
|
229
|
+
reservation.reason ?? ""
|
|
230
|
+
]
|
|
231
|
+
);
|
|
232
|
+
if (result.rows.length > 0) {
|
|
233
|
+
return { success: true };
|
|
234
|
+
}
|
|
235
|
+
const existing = await db.query(
|
|
236
|
+
"SELECT agent_id FROM file_reservations WHERE file_path = $1",
|
|
237
|
+
[reservation.filePath]
|
|
238
|
+
);
|
|
239
|
+
return { success: false, holder: existing.rows[0]?.agent_id };
|
|
240
|
+
}
|
|
241
|
+
/** Check who holds a file reservation. */
|
|
242
|
+
async checkReservation(filePath) {
|
|
243
|
+
const db = this.getDb();
|
|
244
|
+
await db.query("DELETE FROM file_reservations WHERE expires_at < NOW()");
|
|
245
|
+
const result = await db.query(
|
|
246
|
+
"SELECT * FROM file_reservations WHERE file_path = $1",
|
|
247
|
+
[filePath]
|
|
248
|
+
);
|
|
249
|
+
return result.rows[0] ?? null;
|
|
250
|
+
}
|
|
251
|
+
/** Release all reservations held by an agent. */
|
|
252
|
+
async releaseAllReservations(agentId) {
|
|
253
|
+
const db = this.getDb();
|
|
254
|
+
const result = await db.query("DELETE FROM file_reservations WHERE agent_id = $1", [agentId]);
|
|
255
|
+
return result.affectedRows ?? 0;
|
|
256
|
+
}
|
|
257
|
+
/** Get all reservations for an agent. */
|
|
258
|
+
async getReservations(agentId) {
|
|
259
|
+
const db = this.getDb();
|
|
260
|
+
await db.query("DELETE FROM file_reservations WHERE expires_at < NOW()");
|
|
261
|
+
const result = await db.query(
|
|
262
|
+
"SELECT * FROM file_reservations WHERE agent_id = $1 ORDER BY reserved_at",
|
|
263
|
+
[agentId]
|
|
264
|
+
);
|
|
265
|
+
return result.rows;
|
|
266
|
+
}
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Tasks
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
/** Create a new task. */
|
|
271
|
+
async createTask(task) {
|
|
272
|
+
const db = this.getDb();
|
|
273
|
+
const result = await db.query(
|
|
274
|
+
`INSERT INTO tasks (id, description) VALUES ($1, $2)
|
|
275
|
+
ON CONFLICT (id) DO UPDATE SET description = EXCLUDED.description
|
|
276
|
+
RETURNING *`,
|
|
277
|
+
[task.id, task.description]
|
|
278
|
+
);
|
|
279
|
+
return result.rows[0];
|
|
280
|
+
}
|
|
281
|
+
/** Claim a task atomically (CAS: fails if already claimed by another agent). */
|
|
282
|
+
async claimTask(taskId, agentId) {
|
|
283
|
+
const db = this.getDb();
|
|
284
|
+
const result = await db.query(
|
|
285
|
+
`UPDATE tasks SET status = 'claimed', owner = $2, claimed_at = NOW()
|
|
286
|
+
WHERE id = $1 AND (status = 'open' OR owner = $2)
|
|
287
|
+
RETURNING *`,
|
|
288
|
+
[taskId, agentId]
|
|
289
|
+
);
|
|
290
|
+
if (result.rows.length > 0) {
|
|
291
|
+
return { success: true };
|
|
292
|
+
}
|
|
293
|
+
const existing = await db.query("SELECT owner FROM tasks WHERE id = $1", [taskId]);
|
|
294
|
+
if (existing.rows.length === 0) {
|
|
295
|
+
return { success: false, owner: void 0 };
|
|
296
|
+
}
|
|
297
|
+
return { success: false, owner: existing.rows[0].owner ?? void 0 };
|
|
298
|
+
}
|
|
299
|
+
/** Complete a task (only the owner can complete it). */
|
|
300
|
+
async completeTask(taskId, agentId) {
|
|
301
|
+
const db = this.getDb();
|
|
302
|
+
const result = await db.query(
|
|
303
|
+
`UPDATE tasks SET status = 'completed', completed_at = NOW()
|
|
304
|
+
WHERE id = $1 AND owner = $2
|
|
305
|
+
RETURNING id`,
|
|
306
|
+
[taskId, agentId]
|
|
307
|
+
);
|
|
308
|
+
return (result.rows?.length ?? 0) > 0;
|
|
309
|
+
}
|
|
310
|
+
/** Release a claimed task back to open (only the owner can release). */
|
|
311
|
+
async releaseTask(taskId, agentId) {
|
|
312
|
+
const db = this.getDb();
|
|
313
|
+
const result = await db.query(
|
|
314
|
+
`UPDATE tasks SET status = 'open', owner = NULL, claimed_at = NULL
|
|
315
|
+
WHERE id = $1 AND owner = $2
|
|
316
|
+
RETURNING id`,
|
|
317
|
+
[taskId, agentId]
|
|
318
|
+
);
|
|
319
|
+
return (result.rows?.length ?? 0) > 0;
|
|
320
|
+
}
|
|
321
|
+
/** List tasks, optionally filtered by status and/or owner. */
|
|
322
|
+
async listTasks(filter) {
|
|
323
|
+
const db = this.getDb();
|
|
324
|
+
const conditions = [];
|
|
325
|
+
const params = [];
|
|
326
|
+
let paramIdx = 1;
|
|
327
|
+
if (filter?.status) {
|
|
328
|
+
conditions.push(`status = $${paramIdx++}`);
|
|
329
|
+
params.push(filter.status);
|
|
330
|
+
}
|
|
331
|
+
if (filter?.owner) {
|
|
332
|
+
conditions.push(`owner = $${paramIdx++}`);
|
|
333
|
+
params.push(filter.owner);
|
|
334
|
+
}
|
|
335
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
336
|
+
const result = await db.query(
|
|
337
|
+
`SELECT * FROM tasks ${where} ORDER BY created_at`,
|
|
338
|
+
params
|
|
339
|
+
);
|
|
340
|
+
return result.rows;
|
|
341
|
+
}
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Events
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
/** Append an event to the audit log. */
|
|
346
|
+
async logEvent(event) {
|
|
347
|
+
const db = this.getDb();
|
|
348
|
+
const result = await db.query(
|
|
349
|
+
`INSERT INTO events (agent_id, event_type, payload)
|
|
350
|
+
VALUES ($1, $2, $3)
|
|
351
|
+
RETURNING *`,
|
|
352
|
+
[event.agentId, event.eventType, JSON.stringify(event.payload ?? {})]
|
|
353
|
+
);
|
|
354
|
+
return result.rows[0];
|
|
355
|
+
}
|
|
356
|
+
/** Get recent events (newest first). */
|
|
357
|
+
async getRecentEvents(limit) {
|
|
358
|
+
const db = this.getDb();
|
|
359
|
+
const result = await db.query(
|
|
360
|
+
"SELECT * FROM events ORDER BY created_at DESC LIMIT $1",
|
|
361
|
+
[limit]
|
|
362
|
+
);
|
|
363
|
+
return result.rows;
|
|
364
|
+
}
|
|
365
|
+
/** Prune events older than a given number of days. */
|
|
366
|
+
async pruneEvents(olderThanDays) {
|
|
367
|
+
const db = this.getDb();
|
|
368
|
+
const result = await db.query(
|
|
369
|
+
`DELETE FROM events WHERE created_at < NOW() - ($1 || ' days')::INTERVAL`,
|
|
370
|
+
[String(olderThanDays)]
|
|
371
|
+
);
|
|
372
|
+
return result.affectedRows ?? 0;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export {
|
|
377
|
+
SCHEMA_SQL,
|
|
378
|
+
DaemonStore
|
|
379
|
+
};
|
|
380
|
+
//# sourceMappingURL=chunk-DGQ5OB6L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage/schema.ts","../src/storage/daemon-store.ts"],"sourcesContent":["/**\n * PGlite schema for the RevDev Harness daemon.\n *\n * Five tables provide persistent state for multi-agent coordination:\n * - agent_sessions: active and historical agent sessions\n * - agent_messages: inter-agent mailbox (point-to-point + broadcast)\n * - file_reservations: advisory file locks with CAS semantics\n * - tasks: claimable work items with CAS ownership\n * - events: append-only event log for audit trail\n *\n * Uses raw SQL (no Drizzle ORM) to keep the daemon dependency-free.\n * PGlite runs in-process — no external database needed.\n */\n\n/** SQL statements to initialize the daemon database. */\nexport const SCHEMA_SQL = `\n CREATE TABLE IF NOT EXISTS agent_sessions (\n id TEXT PRIMARY KEY,\n env TEXT NOT NULL DEFAULT '',\n task TEXT NOT NULL DEFAULT '(starting)',\n files TEXT NOT NULL DEFAULT '',\n pid INTEGER,\n started_at TIMESTAMP NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMP NOT NULL DEFAULT NOW(),\n ended_at TIMESTAMP,\n exit_summary TEXT\n );\n\n CREATE TABLE IF NOT EXISTS agent_messages (\n id SERIAL PRIMARY KEY,\n from_agent TEXT NOT NULL,\n to_agent TEXT NOT NULL,\n subject TEXT NOT NULL DEFAULT '',\n body TEXT NOT NULL DEFAULT '',\n read BOOLEAN NOT NULL DEFAULT FALSE,\n created_at TIMESTAMP NOT NULL DEFAULT NOW()\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_to_unread\n ON agent_messages (to_agent, read) WHERE read = FALSE;\n\n CREATE TABLE IF NOT EXISTS file_reservations (\n file_path TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL,\n reserved_at TIMESTAMP NOT NULL DEFAULT NOW(),\n expires_at TIMESTAMP NOT NULL,\n reason TEXT NOT NULL DEFAULT ''\n );\n\n CREATE INDEX IF NOT EXISTS idx_reservations_agent\n ON file_reservations (agent_id);\n\n CREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY,\n description TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'open',\n owner TEXT,\n claimed_at TIMESTAMP,\n completed_at TIMESTAMP,\n created_at TIMESTAMP NOT NULL DEFAULT NOW()\n );\n\n CREATE INDEX IF NOT EXISTS idx_tasks_status\n ON tasks (status);\n\n CREATE INDEX IF NOT EXISTS idx_tasks_owner\n ON tasks (owner) WHERE owner IS NOT NULL;\n\n CREATE TABLE IF NOT EXISTS events (\n id SERIAL PRIMARY KEY,\n agent_id TEXT NOT NULL,\n event_type TEXT NOT NULL,\n payload JSONB NOT NULL DEFAULT '{}',\n created_at TIMESTAMP NOT NULL DEFAULT NOW()\n );\n\n CREATE INDEX IF NOT EXISTS idx_events_agent\n ON events (agent_id, created_at DESC);\n`;\n\n/** Session row shape. */\nexport interface AgentSession {\n id: string;\n env: string;\n task: string;\n files: string;\n pid: number | null;\n started_at: string;\n updated_at: string;\n ended_at: string | null;\n exit_summary: string | null;\n}\n\n/** Message row shape. */\nexport interface AgentMessage {\n id: number;\n from_agent: string;\n to_agent: string;\n subject: string;\n body: string;\n read: boolean;\n created_at: string;\n}\n\n/** File reservation row shape. */\nexport interface FileReservation {\n file_path: string;\n agent_id: string;\n reserved_at: string;\n expires_at: string;\n reason: string;\n}\n\n/** Task row shape. */\nexport interface AgentTask {\n id: string;\n description: string;\n status: 'open' | 'claimed' | 'completed';\n owner: string | null;\n claimed_at: string | null;\n completed_at: string | null;\n created_at: string;\n}\n\n/** Event row shape. */\nexport interface DaemonEvent {\n id: number;\n agent_id: string;\n event_type: string;\n payload: Record<string, unknown>;\n created_at: string;\n}\n","/**\n * DaemonStore — persistent state for the RevDev Harness daemon.\n *\n * Backed by PGlite (in-process PostgreSQL). The database file lives at\n * ~/.local/share/revealui/harness.db and survives daemon restarts.\n *\n * All methods are async (PGlite queries return promises).\n */\n\nimport type { PGlite } from '@electric-sql/pglite';\nimport type {\n AgentMessage,\n AgentSession,\n AgentTask,\n DaemonEvent,\n FileReservation,\n} from './schema.js';\nimport { SCHEMA_SQL } from './schema.js';\n\n/** Configuration for DaemonStore. */\nexport interface DaemonStoreConfig {\n /** PGlite data directory (default: ~/.local/share/revealui/harness.db) */\n dataDir: string;\n}\n\nexport class DaemonStore {\n private db: PGlite | null = null;\n private readonly dataDir: string;\n\n constructor(config: DaemonStoreConfig) {\n this.dataDir = config.dataDir;\n }\n\n /** Initialize PGlite and create tables. */\n async init(): Promise<void> {\n const { PGlite: PGliteClass } = await import('@electric-sql/pglite');\n this.db = new PGliteClass(this.dataDir);\n await this.db.exec(SCHEMA_SQL);\n }\n\n /** Shut down the database. */\n async close(): Promise<void> {\n if (this.db) {\n await this.db.close();\n this.db = null;\n }\n }\n\n private getDb(): PGlite {\n if (!this.db) throw new Error('DaemonStore not initialized — call init() first');\n return this.db;\n }\n\n // ---------------------------------------------------------------------------\n // Sessions\n // ---------------------------------------------------------------------------\n\n /** Register or update an agent session. */\n async registerSession(session: {\n id: string;\n env: string;\n task?: string;\n pid?: number;\n }): Promise<AgentSession> {\n const db = this.getDb();\n const result = await db.query<AgentSession>(\n `INSERT INTO agent_sessions (id, env, task, pid)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (id) DO UPDATE SET\n env = EXCLUDED.env,\n task = COALESCE(EXCLUDED.task, agent_sessions.task),\n pid = COALESCE(EXCLUDED.pid, agent_sessions.pid),\n updated_at = NOW(),\n ended_at = NULL\n RETURNING *`,\n [session.id, session.env, session.task ?? '(starting)', session.pid ?? null],\n );\n // RETURNING * always produces a row for INSERT ... ON CONFLICT DO UPDATE\n return result.rows[0] as AgentSession;\n }\n\n /** Update a session's task and/or files. */\n async updateSession(\n id: string,\n updates: { task?: string; files?: string },\n ): Promise<AgentSession | null> {\n const db = this.getDb();\n const sets: string[] = ['updated_at = NOW()'];\n const params: unknown[] = [];\n let paramIdx = 1;\n\n if (updates.task !== undefined) {\n sets.push(`task = $${paramIdx++}`);\n params.push(updates.task);\n }\n if (updates.files !== undefined) {\n sets.push(`files = $${paramIdx++}`);\n params.push(updates.files);\n }\n params.push(id);\n\n const result = await db.query<AgentSession>(\n `UPDATE agent_sessions SET ${sets.join(', ')} WHERE id = $${paramIdx} RETURNING *`,\n params,\n );\n return result.rows[0] ?? null;\n }\n\n /** End a session (mark ended_at, record exit summary). */\n async endSession(id: string, exitSummary?: string): Promise<void> {\n const db = this.getDb();\n await db.query(\n `UPDATE agent_sessions SET ended_at = NOW(), exit_summary = $2, updated_at = NOW()\n WHERE id = $1`,\n [id, exitSummary ?? null],\n );\n }\n\n /** Get all active sessions (ended_at IS NULL). */\n async getActiveSessions(): Promise<AgentSession[]> {\n const db = this.getDb();\n const result = await db.query<AgentSession>(\n 'SELECT * FROM agent_sessions WHERE ended_at IS NULL ORDER BY started_at',\n );\n return result.rows;\n }\n\n /** Get session history for an agent (most recent first). */\n async getSessionHistory(agentId: string, limit: number): Promise<AgentSession[]> {\n const db = this.getDb();\n const result = await db.query<AgentSession>(\n 'SELECT * FROM agent_sessions WHERE id = $1 ORDER BY started_at DESC LIMIT $2',\n [agentId, limit],\n );\n return result.rows;\n }\n\n // ---------------------------------------------------------------------------\n // Messages\n // ---------------------------------------------------------------------------\n\n /** Send a message from one agent to another. */\n async sendMessage(msg: {\n fromAgent: string;\n toAgent: string;\n subject: string;\n body?: string;\n }): Promise<AgentMessage> {\n const db = this.getDb();\n const result = await db.query<AgentMessage>(\n `INSERT INTO agent_messages (from_agent, to_agent, subject, body)\n VALUES ($1, $2, $3, $4)\n RETURNING *`,\n [msg.fromAgent, msg.toAgent, msg.subject, msg.body ?? ''],\n );\n // RETURNING * always produces a row for a plain INSERT\n return result.rows[0] as AgentMessage;\n }\n\n /** Broadcast a message to all active agents (except sender). */\n async broadcastMessage(msg: {\n fromAgent: string;\n subject: string;\n body?: string;\n }): Promise<number> {\n const active = await this.getActiveSessions();\n let sent = 0;\n for (const session of active) {\n if (session.id === msg.fromAgent) continue;\n await this.sendMessage({\n fromAgent: msg.fromAgent,\n toAgent: session.id,\n subject: msg.subject,\n body: msg.body,\n });\n sent++;\n }\n return sent;\n }\n\n /** Get unread messages for an agent. */\n async getInbox(agentId: string, unreadOnly: boolean): Promise<AgentMessage[]> {\n const db = this.getDb();\n const whereClause = unreadOnly ? 'WHERE to_agent = $1 AND read = FALSE' : 'WHERE to_agent = $1';\n const result = await db.query<AgentMessage>(\n `SELECT * FROM agent_messages ${whereClause} ORDER BY created_at DESC LIMIT 50`,\n [agentId],\n );\n return result.rows;\n }\n\n /** Mark messages as read. */\n async markRead(messageIds: number[]): Promise<void> {\n if (messageIds.length === 0) return;\n const db = this.getDb();\n const placeholders = messageIds.map((_, i) => `$${i + 1}`).join(', ');\n await db.query(\n `UPDATE agent_messages SET read = TRUE WHERE id IN (${placeholders})`,\n messageIds,\n );\n }\n\n // ---------------------------------------------------------------------------\n // File Reservations\n // ---------------------------------------------------------------------------\n\n /** Reserve a file for an agent (CAS: fails if already reserved by another). */\n async reserveFile(reservation: {\n filePath: string;\n agentId: string;\n ttlSeconds: number;\n reason?: string;\n }): Promise<{ success: boolean; holder?: string }> {\n const db = this.getDb();\n\n // Clean expired reservations first\n await db.query('DELETE FROM file_reservations WHERE expires_at < NOW()');\n\n // Try to insert (CAS semantics via ON CONFLICT)\n const result = await db.query<FileReservation>(\n `INSERT INTO file_reservations (file_path, agent_id, expires_at, reason)\n VALUES ($1, $2, NOW() + ($3 || ' seconds')::INTERVAL, $4)\n ON CONFLICT (file_path) DO UPDATE SET\n agent_id = EXCLUDED.agent_id,\n reserved_at = NOW(),\n expires_at = EXCLUDED.expires_at,\n reason = EXCLUDED.reason\n WHERE file_reservations.agent_id = $2 OR file_reservations.expires_at < NOW()\n RETURNING *`,\n [\n reservation.filePath,\n reservation.agentId,\n String(reservation.ttlSeconds),\n reservation.reason ?? '',\n ],\n );\n\n if (result.rows.length > 0) {\n return { success: true };\n }\n\n // Someone else holds it — who?\n const existing = await db.query<FileReservation>(\n 'SELECT agent_id FROM file_reservations WHERE file_path = $1',\n [reservation.filePath],\n );\n return { success: false, holder: existing.rows[0]?.agent_id };\n }\n\n /** Check who holds a file reservation. */\n async checkReservation(filePath: string): Promise<FileReservation | null> {\n const db = this.getDb();\n // Clean expired first\n await db.query('DELETE FROM file_reservations WHERE expires_at < NOW()');\n const result = await db.query<FileReservation>(\n 'SELECT * FROM file_reservations WHERE file_path = $1',\n [filePath],\n );\n return result.rows[0] ?? null;\n }\n\n /** Release all reservations held by an agent. */\n async releaseAllReservations(agentId: string): Promise<number> {\n const db = this.getDb();\n const result = await db.query('DELETE FROM file_reservations WHERE agent_id = $1', [agentId]);\n return result.affectedRows ?? 0;\n }\n\n /** Get all reservations for an agent. */\n async getReservations(agentId: string): Promise<FileReservation[]> {\n const db = this.getDb();\n await db.query('DELETE FROM file_reservations WHERE expires_at < NOW()');\n const result = await db.query<FileReservation>(\n 'SELECT * FROM file_reservations WHERE agent_id = $1 ORDER BY reserved_at',\n [agentId],\n );\n return result.rows;\n }\n\n // ---------------------------------------------------------------------------\n // Tasks\n // ---------------------------------------------------------------------------\n\n /** Create a new task. */\n async createTask(task: { id: string; description: string }): Promise<AgentTask> {\n const db = this.getDb();\n const result = await db.query<AgentTask>(\n `INSERT INTO tasks (id, description) VALUES ($1, $2)\n ON CONFLICT (id) DO UPDATE SET description = EXCLUDED.description\n RETURNING *`,\n [task.id, task.description],\n );\n return result.rows[0] as AgentTask;\n }\n\n /** Claim a task atomically (CAS: fails if already claimed by another agent). */\n async claimTask(taskId: string, agentId: string): Promise<{ success: boolean; owner?: string }> {\n const db = this.getDb();\n // Atomic: only claim if status is 'open' OR already owned by the same agent\n const result = await db.query<AgentTask>(\n `UPDATE tasks SET status = 'claimed', owner = $2, claimed_at = NOW()\n WHERE id = $1 AND (status = 'open' OR owner = $2)\n RETURNING *`,\n [taskId, agentId],\n );\n if (result.rows.length > 0) {\n return { success: true };\n }\n // Someone else owns it — who?\n const existing = await db.query<AgentTask>('SELECT owner FROM tasks WHERE id = $1', [taskId]);\n if (existing.rows.length === 0) {\n return { success: false, owner: undefined };\n }\n return { success: false, owner: (existing.rows[0] as AgentTask).owner ?? undefined };\n }\n\n /** Complete a task (only the owner can complete it). */\n async completeTask(taskId: string, agentId: string): Promise<boolean> {\n const db = this.getDb();\n const result = await db.query(\n `UPDATE tasks SET status = 'completed', completed_at = NOW()\n WHERE id = $1 AND owner = $2\n RETURNING id`,\n [taskId, agentId],\n );\n return (result.rows?.length ?? 0) > 0;\n }\n\n /** Release a claimed task back to open (only the owner can release). */\n async releaseTask(taskId: string, agentId: string): Promise<boolean> {\n const db = this.getDb();\n const result = await db.query(\n `UPDATE tasks SET status = 'open', owner = NULL, claimed_at = NULL\n WHERE id = $1 AND owner = $2\n RETURNING id`,\n [taskId, agentId],\n );\n return (result.rows?.length ?? 0) > 0;\n }\n\n /** List tasks, optionally filtered by status and/or owner. */\n async listTasks(filter?: { status?: string; owner?: string }): Promise<AgentTask[]> {\n const db = this.getDb();\n const conditions: string[] = [];\n const params: unknown[] = [];\n let paramIdx = 1;\n\n if (filter?.status) {\n conditions.push(`status = $${paramIdx++}`);\n params.push(filter.status);\n }\n if (filter?.owner) {\n conditions.push(`owner = $${paramIdx++}`);\n params.push(filter.owner);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const result = await db.query<AgentTask>(\n `SELECT * FROM tasks ${where} ORDER BY created_at`,\n params,\n );\n return result.rows;\n }\n\n // ---------------------------------------------------------------------------\n // Events\n // ---------------------------------------------------------------------------\n\n /** Append an event to the audit log. */\n async logEvent(event: {\n agentId: string;\n eventType: string;\n payload?: Record<string, unknown>;\n }): Promise<DaemonEvent> {\n const db = this.getDb();\n const result = await db.query<DaemonEvent>(\n `INSERT INTO events (agent_id, event_type, payload)\n VALUES ($1, $2, $3)\n RETURNING *`,\n [event.agentId, event.eventType, JSON.stringify(event.payload ?? {})],\n );\n // RETURNING * always produces a row for a plain INSERT\n return result.rows[0] as DaemonEvent;\n }\n\n /** Get recent events (newest first). */\n async getRecentEvents(limit: number): Promise<DaemonEvent[]> {\n const db = this.getDb();\n const result = await db.query<DaemonEvent>(\n 'SELECT * FROM events ORDER BY created_at DESC LIMIT $1',\n [limit],\n );\n return result.rows;\n }\n\n /** Prune events older than a given number of days. */\n async pruneEvents(olderThanDays: number): Promise<number> {\n const db = this.getDb();\n const result = await db.query(\n `DELETE FROM events WHERE created_at < NOW() - ($1 || ' days')::INTERVAL`,\n [String(olderThanDays)],\n );\n return result.affectedRows ?? 0;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -58,7 +58,7 @@ Detect documentation drift \u2014 places where code has changed but docs haven't
|
|
|
58
58
|
- Flag undocumented commands or removed commands still in docs
|
|
59
59
|
|
|
60
60
|
### 4. Collection Fields \u2194 CMS Docs
|
|
61
|
-
- Compare \`apps/
|
|
61
|
+
- Compare \`apps/admin/src/collections/\` field definitions against any collection docs
|
|
62
62
|
- Flag field additions/removals not reflected in documentation
|
|
63
63
|
|
|
64
64
|
### 5. Environment Variables
|
|
@@ -355,7 +355,7 @@ var stripeBestPracticesCommand = {
|
|
|
355
355
|
- Log \`stripe_error\` codes for debugging; never expose raw error objects to clients
|
|
356
356
|
|
|
357
357
|
## RevealUI-Specific
|
|
358
|
-
- Webhook endpoint: \`apps/
|
|
358
|
+
- Webhook endpoint: \`apps/admin\` at \`/api/webhooks/stripe\` (NOT the API endpoint)
|
|
359
359
|
- Stripe service: \`packages/services/src/stripe/\`
|
|
360
360
|
- Billing routes: \`apps/api/src/routes/billing.ts\`
|
|
361
361
|
- Price IDs are managed via \`pnpm stripe:seed\` \u2014 see \`scripts/setup/seed-stripe.ts\``
|
|
@@ -569,8 +569,8 @@ RevealUI uses **two databases with strictly separated responsibilities**:
|
|
|
569
569
|
- \`packages/core/\` \u2014 CMS engine must be DB-agnostic
|
|
570
570
|
- \`packages/contracts/\` \u2014 contracts are schema-only
|
|
571
571
|
- \`packages/config/\` \u2014 config must not hardcode DB client
|
|
572
|
-
- \`apps/
|
|
573
|
-
- \`apps/
|
|
572
|
+
- \`apps/admin/src/collections/\` \u2014 collection hooks use Drizzle/Neon only
|
|
573
|
+
- \`apps/admin/src/routes/\` \u2014 REST routes use Neon only
|
|
574
574
|
|
|
575
575
|
## Schema Organization
|
|
576
576
|
|
|
@@ -773,7 +773,7 @@ When the Skill tool is available, proactively invoke the following skills in the
|
|
|
773
773
|
|
|
774
774
|
- \`/vercel-react-best-practices\` \u2014 before completing any PR that touches React components or hooks
|
|
775
775
|
- \`/stripe-best-practices\` \u2014 any time you write or modify billing, payment, webhook, or Stripe code
|
|
776
|
-
- \`/next-best-practices\` \u2014 when implementing features in apps/
|
|
776
|
+
- \`/next-best-practices\` \u2014 when implementing features in apps/admin or apps/marketing
|
|
777
777
|
- \`/next-cache-components\` \u2014 when adding 'use cache', cache profiles, or PPR to a Next.js route
|
|
778
778
|
- \`/vercel-composition-patterns\` \u2014 when adding new components to @revealui/presentation
|
|
779
779
|
- \`/web-design-guidelines\` \u2014 when asked to review a UI, page, or component for quality
|
|
@@ -932,7 +932,7 @@ In v4, theme tokens go in CSS, not JS:
|
|
|
932
932
|
|
|
933
933
|
### Consumer Pattern (current \u2014 v3 compat)
|
|
934
934
|
\`\`\`ts
|
|
935
|
-
// apps/
|
|
935
|
+
// apps/admin/tailwind.config.ts
|
|
936
936
|
import { createTailwindConfig } from 'dev/tailwind/create-config'
|
|
937
937
|
export default createTailwindConfig({
|
|
938
938
|
content: ['./src/**/*.{ts,tsx}'],
|
|
@@ -1589,8 +1589,8 @@ RevealUI uses **two databases with strictly separated responsibilities**:
|
|
|
1589
1589
|
- \`packages/core/\` \u2014 CMS engine must be DB-agnostic
|
|
1590
1590
|
- \`packages/contracts/\` \u2014 contracts are schema-only
|
|
1591
1591
|
- \`packages/config/\` \u2014 config must not hardcode DB client
|
|
1592
|
-
- \`apps/
|
|
1593
|
-
- \`apps/
|
|
1592
|
+
- \`apps/admin/src/collections/\` \u2014 collection hooks use Drizzle/Neon only
|
|
1593
|
+
- \`apps/admin/src/routes/\` \u2014 REST routes use Neon only
|
|
1594
1594
|
|
|
1595
1595
|
## Schema Organization
|
|
1596
1596
|
|
|
@@ -1815,7 +1815,7 @@ Follow these rules for ALL code changes in the RevealUI monorepo.
|
|
|
1815
1815
|
- \`packages/services/src/supabase/\`
|
|
1816
1816
|
- \`apps/*/src/lib/supabase/\`
|
|
1817
1817
|
|
|
1818
|
-
FORBIDDEN in: \`packages/core/\`, \`packages/contracts/\`, \`packages/config/\`, \`apps/
|
|
1818
|
+
FORBIDDEN in: \`packages/core/\`, \`packages/contracts/\`, \`packages/config/\`, \`apps/admin/src/collections/\`, \`apps/admin/src/routes/\`
|
|
1819
1819
|
|
|
1820
1820
|
## Code Quality
|
|
1821
1821
|
|
|
@@ -2534,4 +2534,4 @@ export {
|
|
|
2534
2534
|
diffContent,
|
|
2535
2535
|
listContent
|
|
2536
2536
|
};
|
|
2537
|
-
//# sourceMappingURL=chunk-
|
|
2537
|
+
//# sourceMappingURL=chunk-PROC6EJC.js.map
|