@revealui/harnesses 0.1.7 → 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.
@@ -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/cms/src/collections/\` field definitions against any collection docs
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/cms\` at \`/api/webhooks/stripe\` (NOT the API endpoint)
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/cms/src/collections/\` \u2014 collection hooks use Drizzle/Neon only
573
- - \`apps/cms/src/routes/\` \u2014 REST routes use Neon only
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/cms or apps/marketing
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/cms/tailwind.config.ts
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/cms/src/collections/\` \u2014 collection hooks use Drizzle/Neon only
1593
- - \`apps/cms/src/routes/\` \u2014 REST routes use Neon only
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/cms/src/collections/\`, \`apps/cms/src/routes/\`
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-XXEKWC6F.js.map
2537
+ //# sourceMappingURL=chunk-PROC6EJC.js.map