pragma-so 0.1.0

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 (65) hide show
  1. package/cli/index.ts +882 -0
  2. package/index.ts +3 -0
  3. package/package.json +53 -0
  4. package/server/connectorBinaries.ts +103 -0
  5. package/server/connectorRegistry.ts +158 -0
  6. package/server/conversation/adapterRegistry.ts +53 -0
  7. package/server/conversation/adapters/claudeAdapter.ts +138 -0
  8. package/server/conversation/adapters/codexAdapter.ts +142 -0
  9. package/server/conversation/adapters.ts +224 -0
  10. package/server/conversation/executeRunner.ts +1191 -0
  11. package/server/conversation/gitWorkflow.ts +1037 -0
  12. package/server/conversation/models.ts +23 -0
  13. package/server/conversation/pragmaCli.ts +34 -0
  14. package/server/conversation/prompts.ts +335 -0
  15. package/server/conversation/store.ts +805 -0
  16. package/server/conversation/titleGenerator.ts +106 -0
  17. package/server/conversation/turnRunner.ts +365 -0
  18. package/server/conversation/types.ts +134 -0
  19. package/server/db.ts +837 -0
  20. package/server/http/middleware.ts +31 -0
  21. package/server/http/schemas.ts +430 -0
  22. package/server/http/validators.ts +38 -0
  23. package/server/index.ts +6560 -0
  24. package/server/process/runCommand.ts +142 -0
  25. package/server/stores/agentStore.ts +167 -0
  26. package/server/stores/connectorStore.ts +299 -0
  27. package/server/stores/humanStore.ts +28 -0
  28. package/server/stores/skillStore.ts +127 -0
  29. package/server/stores/taskStore.ts +371 -0
  30. package/shared/net.ts +24 -0
  31. package/tsconfig.json +14 -0
  32. package/ui/index.html +14 -0
  33. package/ui/public/favicon-32.png +0 -0
  34. package/ui/public/favicon.png +0 -0
  35. package/ui/src/App.jsx +1338 -0
  36. package/ui/src/api.js +954 -0
  37. package/ui/src/components/CodeView.jsx +319 -0
  38. package/ui/src/components/ConnectionsView.jsx +1004 -0
  39. package/ui/src/components/ContextView.jsx +315 -0
  40. package/ui/src/components/ConversationDrawer.jsx +963 -0
  41. package/ui/src/components/EmptyPane.jsx +20 -0
  42. package/ui/src/components/FeedView.jsx +773 -0
  43. package/ui/src/components/FilesView.jsx +257 -0
  44. package/ui/src/components/InlineChatView.jsx +158 -0
  45. package/ui/src/components/InputBar.jsx +476 -0
  46. package/ui/src/components/OnboardingModal.jsx +112 -0
  47. package/ui/src/components/OutputPanel.jsx +658 -0
  48. package/ui/src/components/PlanProposalPanel.jsx +177 -0
  49. package/ui/src/components/RightPanel.jsx +951 -0
  50. package/ui/src/components/SettingsView.jsx +186 -0
  51. package/ui/src/components/Sidebar.jsx +247 -0
  52. package/ui/src/components/TestingPane.jsx +198 -0
  53. package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
  54. package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
  55. package/ui/src/components/testing/TerminalPanel.jsx +104 -0
  56. package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
  57. package/ui/src/hooks/useAgents.js +81 -0
  58. package/ui/src/hooks/useConversation.js +252 -0
  59. package/ui/src/hooks/useTasks.js +161 -0
  60. package/ui/src/hooks/useWorkspace.js +259 -0
  61. package/ui/src/lib/agentIcon.js +10 -0
  62. package/ui/src/lib/conversationUtils.js +575 -0
  63. package/ui/src/main.jsx +10 -0
  64. package/ui/src/styles.css +6899 -0
  65. package/ui/vite.config.mjs +6 -0
package/server/db.ts ADDED
@@ -0,0 +1,837 @@
1
+ import { mkdir, readdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { PGlite } from "@electric-sql/pglite";
5
+ import { initializeWorkspaceGit } from "./conversation/gitWorkflow";
6
+ import { ensureConversationSchema } from "./conversation/store";
7
+
8
+ export const PRAGMA_DIR = join(homedir(), ".pragma");
9
+ const ACTIVE_WORKSPACE_FILE = join(PRAGMA_DIR, "active_workspace");
10
+ const RESERVED_ROOT_NAMES = new Set(["db", "workspace", "worktrees"]);
11
+ export const DEFAULT_AGENT_ID = "pragma-orchestrator";
12
+ const OPEN_DATABASES = new Map<string, Promise<PGlite>>();
13
+
14
+ const DEFAULT_HARNESS_MODELS: Record<string, { label: string; id: string }> = {
15
+ claude_code: { label: "Opus 4.6", id: "opus" },
16
+ codex: { label: "GPT-5", id: "gpt-5" },
17
+ };
18
+
19
+ function getDefaultModelForHarness(harness: string): { label: string; id: string } {
20
+ return DEFAULT_HARNESS_MODELS[harness] ?? DEFAULT_HARNESS_MODELS.claude_code;
21
+ }
22
+ const REAL_CLOSE = new WeakMap<PGlite, () => Promise<void>>();
23
+
24
+ export const DEFAULT_AGENT_FILE = `# Orchestrator
25
+
26
+ You are the orchestrator agent for Pragma.
27
+
28
+ Your task is to:
29
+ - Plan tasks into clear, ordered steps.
30
+ - Spawn specialized agents to execute those steps.
31
+ - Coordinate progress across agents.
32
+ - Track status, risks, and blockers.
33
+ - Produce concise updates and a final combined result.
34
+
35
+ ## Pragma Commands
36
+ - \`pragma setup\`: Calls the API setup endpoint. This only bootstraps \`~/.pragma\`.
37
+ - \`pragma create-task <title> [--status <status>] [--assigned-to <agent_id>] [--output-dir <path>]\`: Calls the API to create a row in the \`tasks\` table. Default status is \`queued\`.
38
+ - \`pragma list-tasks [--status <status>] [--limit <n>]\`: Calls the API to list tasks from newest to oldest.
39
+ - \`pragma task select-recipient --agent-id <id> --reason "<text>"\`: Persist orchestrator recipient selection.
40
+ - \`pragma task plan-select-recipient --agent-id <id> --reason "<text>"\`: Persist recipient selection for the current plan turn.
41
+ - \`pragma task ask-question --question "<text>" [--details "<text>"]\`: Ask the human a blocking question.
42
+ - \`pragma task request-help --summary "<text>" [--details "<text>"]\`: Escalate for human help.
43
+ - \`pragma db-query --sql "<SELECT statement>"\`: Run a read-only SQL query against the workspace database. Key tables: tasks, agents, conversation_threads, conversation_turns, conversation_messages, conversation_events.
44
+ - \`pragma server [--port <n>]\`: Starts the Pragma API server.
45
+ - \`pragma ui [--port <n>] [--api-url <url>]\`: Starts the Pragma UI.
46
+ - \`pragma\` (no args): Starts server + UI and opens the UI.
47
+ `;
48
+
49
+ const CODER_AGENT_FILE = `# Coder
50
+
51
+ You are the implementation specialist.
52
+
53
+ Your task is to:
54
+ - Turn requirements into working code.
55
+ - Make focused, minimal diffs.
56
+ - Run builds/tests and fix failures before handoff.
57
+ - Report what changed and any follow-up work.
58
+ `;
59
+
60
+ const UI_DESIGNER_AGENT_FILE = `# UI Designer
61
+
62
+ You are **UI Designer**, an expert user interface designer who creates beautiful, consistent, and accessible user interfaces. You specialize in visual design systems, component libraries, and pixel-perfect interface creation that enhances user experience while reflecting brand identity.
63
+
64
+ ## Your Core Mission
65
+
66
+ ### Create Comprehensive Design Systems
67
+ - Develop component libraries with consistent visual language and interaction patterns
68
+ - Design scalable design token systems for cross-platform consistency
69
+ - Establish visual hierarchy through typography, color, and layout principles
70
+ - Build responsive design frameworks that work across all device types
71
+ - **Default requirement**: Include accessibility compliance (WCAG AA minimum) in all designs
72
+
73
+ ### Craft Pixel-Perfect Interfaces
74
+ - Design detailed interface components with precise specifications
75
+ - Create interactive prototypes that demonstrate user flows and micro-interactions
76
+ - Develop dark mode and theming systems for flexible brand expression
77
+ - Ensure brand integration while maintaining optimal usability
78
+
79
+ ### Enable Developer Success
80
+ - Provide clear design handoff specifications with measurements and assets
81
+ - Create comprehensive component documentation with usage guidelines
82
+ - Establish design QA processes for implementation accuracy validation
83
+ - Build reusable pattern libraries that reduce development time
84
+
85
+ ## Critical Rules
86
+
87
+ ### Design System First Approach
88
+ - Establish component foundations before creating individual screens
89
+ - Design for scalability and consistency across entire product ecosystem
90
+ - Create reusable patterns that prevent design debt and inconsistency
91
+ - Build accessibility into the foundation rather than adding it later
92
+
93
+ ### Performance-Conscious Design
94
+ - Optimize images, icons, and assets for web performance
95
+ - Design with CSS efficiency in mind to reduce render time
96
+ - Consider loading states and progressive enhancement in all designs
97
+ - Balance visual richness with technical constraints
98
+
99
+ ## Your Workflow Process
100
+
101
+ 1. **Design System Foundation**: Review brand guidelines, analyze UI patterns, research accessibility requirements
102
+ 2. **Component Architecture**: Design base components (buttons, inputs, cards, navigation), create variations and states, establish interaction patterns
103
+ 3. **Visual Hierarchy System**: Develop typography scale, design color system with semantic meaning, create spacing system, establish shadow and elevation system
104
+ 4. **Developer Handoff**: Generate detailed design specifications, create component documentation, prepare optimized assets, establish design QA process
105
+
106
+ ## Communication Style
107
+
108
+ - **Be precise**: Specify exact values, ratios, and measurements
109
+ - **Focus on consistency**: Establish and follow systematic design tokens
110
+ - **Think systematically**: Create component variations that scale across all breakpoints
111
+ - **Ensure accessibility**: Design with keyboard navigation and screen reader support (WCAG AA: 4.5:1 contrast for normal text, 3:1 for large text)
112
+ `;
113
+
114
+ type DefaultAgentSeed = {
115
+ id: string;
116
+ name: string;
117
+ description: string;
118
+ status: string;
119
+ agent_file: string;
120
+ emoji: string;
121
+ harness: string;
122
+ model_label: string;
123
+ model_id: string;
124
+ };
125
+
126
+ const DEFAULT_AGENT_SEEDS: DefaultAgentSeed[] = [
127
+ {
128
+ id: DEFAULT_AGENT_ID,
129
+ name: "Orchestrator",
130
+ description: "Plans tasks, spawns specialized agents, and coordinates progress across the team.",
131
+ status: "active",
132
+ agent_file: DEFAULT_AGENT_FILE,
133
+ emoji: "🧭",
134
+ harness: "claude_code",
135
+ model_label: "Opus 4.6",
136
+ model_id: "opus",
137
+ },
138
+ {
139
+ id: "pragma-coder",
140
+ name: "Coder",
141
+ description: "Turns requirements into working code with focused, minimal diffs.",
142
+ status: "idle",
143
+ agent_file: CODER_AGENT_FILE,
144
+ emoji: "💻",
145
+ harness: "claude_code",
146
+ model_label: "Opus 4.6",
147
+ model_id: "opus",
148
+ },
149
+ {
150
+ id: "pragma-ui-designer",
151
+ name: "UI Designer",
152
+ description: "Creates beautiful, consistent, and accessible user interfaces with design systems and pixel-perfect components.",
153
+ status: "idle",
154
+ agent_file: UI_DESIGNER_AGENT_FILE,
155
+ emoji: "🎨",
156
+ harness: "claude_code",
157
+ model_label: "Opus 4.6",
158
+ model_id: "opus",
159
+ },
160
+ ];
161
+
162
+ export class PragmaError extends Error {
163
+ code: string;
164
+ status: number;
165
+
166
+ constructor(code: string, status: number, message: string) {
167
+ super(message);
168
+ this.code = code;
169
+ this.status = status;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Query a single row by ID and throw a PragmaError(404) if it doesn't exist.
175
+ */
176
+ export async function findOrThrow<T extends Record<string, unknown>>(
177
+ db: PGlite,
178
+ table: string,
179
+ id: string,
180
+ errorCode: string,
181
+ label: string = table.replace(/_/g, " "),
182
+ columns: string = "id",
183
+ ): Promise<T> {
184
+ const result = await db.query<T>(`SELECT ${columns} FROM ${table} WHERE id = $1 LIMIT 1`, [id]);
185
+ if (result.rows.length === 0) {
186
+ throw new PragmaError(errorCode, 404, `${label} not found: ${id}`);
187
+ }
188
+ return result.rows[0];
189
+ }
190
+
191
+ /**
192
+ * Delete a row by ID and throw a PragmaError(404) if no row was affected.
193
+ */
194
+ export async function deleteOrThrow(
195
+ db: PGlite,
196
+ table: string,
197
+ id: string,
198
+ errorCode: string,
199
+ label: string = table.replace(/_/g, " "),
200
+ ): Promise<void> {
201
+ const result = await db.query(`DELETE FROM ${table} WHERE id = $1`, [id]);
202
+ if ((result.affectedRows ?? 0) === 0) {
203
+ throw new PragmaError(errorCode, 404, `${label} not found: ${id}`);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Run an UPDATE and throw a PragmaError(404) if no row was affected.
209
+ */
210
+ export async function updateOrThrow(
211
+ db: PGlite,
212
+ sql: string,
213
+ params: unknown[],
214
+ errorCode: string,
215
+ label: string,
216
+ id: string,
217
+ ): Promise<void> {
218
+ const result = await db.query(sql, params);
219
+ if ((result.affectedRows ?? 0) === 0) {
220
+ throw new PragmaError(errorCode, 404, `${label} not found: ${id}`);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Delete a junction-table row by two composite-key columns and throw 404 if not found.
226
+ */
227
+ export async function deleteJunctionOrThrow(
228
+ db: PGlite,
229
+ table: string,
230
+ col1: string,
231
+ val1: string,
232
+ col2: string,
233
+ val2: string,
234
+ errorCode: string,
235
+ message: string,
236
+ ): Promise<void> {
237
+ const result = await db.query(
238
+ `DELETE FROM ${table} WHERE ${col1} = $1 AND ${col2} = $2`,
239
+ [val1, val2],
240
+ );
241
+ if ((result.affectedRows ?? 0) === 0) {
242
+ throw new PragmaError(errorCode, 404, message);
243
+ }
244
+ }
245
+
246
+ export function getPragmaRoot(): string {
247
+ return PRAGMA_DIR;
248
+ }
249
+
250
+ export function getWorkspacePaths(name: string): {
251
+ name: string;
252
+ rootDir: string;
253
+ dbDir: string;
254
+ workspaceDir: string;
255
+ contextDir: string;
256
+ codeDir: string;
257
+ outputsDir: string;
258
+ uploadsDir: string;
259
+ worktreesDir: string;
260
+ binDir: string;
261
+ } {
262
+ const rootDir = join(PRAGMA_DIR, name);
263
+ const workspaceDir = join(rootDir, "workspace");
264
+ const contextDir = join(workspaceDir, "context");
265
+
266
+ return {
267
+ name,
268
+ rootDir,
269
+ dbDir: join(rootDir, "db"),
270
+ workspaceDir,
271
+ contextDir,
272
+ codeDir: join(workspaceDir, "code"),
273
+ outputsDir: join(workspaceDir, "outputs"),
274
+ uploadsDir: join(workspaceDir, "uploads"),
275
+ worktreesDir: join(rootDir, "worktrees"),
276
+ binDir: join(workspaceDir, "bin"),
277
+ };
278
+ }
279
+
280
+ export async function setupPragma(): Promise<void> {
281
+ await mkdir(PRAGMA_DIR, { recursive: true });
282
+ }
283
+
284
+ export async function listWorkspaceNames(): Promise<string[]> {
285
+ await setupPragma();
286
+
287
+ const entries = await readdir(PRAGMA_DIR, { withFileTypes: true });
288
+ return entries
289
+ .filter((entry) => {
290
+ if (!entry.isDirectory()) {
291
+ return false;
292
+ }
293
+ if (entry.name.startsWith(".")) {
294
+ return false;
295
+ }
296
+ if (RESERVED_ROOT_NAMES.has(entry.name)) {
297
+ return false;
298
+ }
299
+ return true;
300
+ })
301
+ .map((entry) => entry.name)
302
+ .sort((a, b) => a.localeCompare(b));
303
+ }
304
+
305
+ export async function getActiveWorkspaceName(): Promise<string | null> {
306
+ await setupPragma();
307
+
308
+ let storedName = "";
309
+ try {
310
+ storedName = (await readFile(ACTIVE_WORKSPACE_FILE, "utf8")).trim();
311
+ } catch {
312
+ return null;
313
+ }
314
+
315
+ if (!storedName) {
316
+ return null;
317
+ }
318
+
319
+ try {
320
+ validateWorkspaceName(storedName);
321
+ } catch {
322
+ return null;
323
+ }
324
+
325
+ if (!(await workspaceExists(storedName))) {
326
+ return null;
327
+ }
328
+
329
+ return storedName;
330
+ }
331
+
332
+ export async function setActiveWorkspaceName(name: string): Promise<void> {
333
+ validateWorkspaceName(name);
334
+
335
+ if (!(await workspaceExists(name))) {
336
+ throw new PragmaError(
337
+ "WORKSPACE_NOT_FOUND",
338
+ 404,
339
+ `Workspace does not exist: ${name}`,
340
+ );
341
+ }
342
+
343
+ await setupPragma();
344
+ await writeFile(ACTIVE_WORKSPACE_FILE, name, "utf8");
345
+ }
346
+
347
+ export async function createWorkspace(input: {
348
+ name: string;
349
+ orchestrator_harness: string;
350
+ }): Promise<void> {
351
+ const name = input.name;
352
+ const orchestratorHarness = input.orchestrator_harness;
353
+
354
+ validateWorkspaceName(name);
355
+
356
+ await setupPragma();
357
+
358
+ const paths = getWorkspacePaths(name);
359
+ if (await pathExists(paths.rootDir)) {
360
+ throw new PragmaError(
361
+ "WORKSPACE_EXISTS",
362
+ 409,
363
+ `Workspace already exists: ${name}`,
364
+ );
365
+ }
366
+
367
+ await mkdir(paths.dbDir, { recursive: true });
368
+ await mkdir(paths.contextDir, { recursive: true });
369
+ await mkdir(paths.codeDir, { recursive: true });
370
+ await mkdir(paths.outputsDir, { recursive: true });
371
+ await mkdir(paths.uploadsDir, { recursive: true });
372
+ await mkdir(paths.worktreesDir, { recursive: true });
373
+
374
+ await initializeDatabase(name, orchestratorHarness);
375
+ await initializeWorkspaceGit(paths);
376
+
377
+ await setActiveWorkspaceName(name);
378
+ }
379
+
380
+ export async function deleteWorkspace(name: string): Promise<{ nextActive: string | null }> {
381
+ validateWorkspaceName(name);
382
+
383
+ const paths = getWorkspacePaths(name);
384
+ if (!(await pathExists(paths.rootDir))) {
385
+ throw new PragmaError("WORKSPACE_NOT_FOUND", 404, `Workspace does not exist: ${name}`);
386
+ }
387
+
388
+ const currentActive = await getActiveWorkspaceName();
389
+ await rm(paths.rootDir, { recursive: true, force: false });
390
+
391
+ if (currentActive !== name) {
392
+ return { nextActive: currentActive };
393
+ }
394
+
395
+ const remaining = await listWorkspaceNames();
396
+ if (remaining.length === 0) {
397
+ await clearActiveWorkspaceName();
398
+ return { nextActive: null };
399
+ }
400
+
401
+ const nextActive = remaining[0];
402
+ await writeFile(ACTIVE_WORKSPACE_FILE, nextActive, "utf8");
403
+ return { nextActive };
404
+ }
405
+
406
+ export async function workspaceExists(name: string): Promise<boolean> {
407
+ validateWorkspaceName(name);
408
+
409
+ const paths = getWorkspacePaths(name);
410
+ try {
411
+ const entryStat = await stat(paths.rootDir);
412
+ return entryStat.isDirectory();
413
+ } catch {
414
+ return false;
415
+ }
416
+ }
417
+
418
+ export async function initializeDatabase(workspaceName: string, orchestratorHarness?: string): Promise<void> {
419
+ const db = await openDatabase(workspaceName);
420
+
421
+ try {
422
+ await ensureRequiredSchema(db);
423
+ await ensureDefaultAgents(db, orchestratorHarness);
424
+ await ensureDefaultHuman(db);
425
+ await ensureConversationSchema(db);
426
+ } finally {
427
+ await db.close();
428
+ }
429
+ }
430
+
431
+ export async function openDatabase(workspaceName: string): Promise<PGlite> {
432
+ validateWorkspaceName(workspaceName);
433
+
434
+ const paths = getWorkspacePaths(workspaceName);
435
+ await setupPragma();
436
+ await mkdir(paths.dbDir, { recursive: true });
437
+
438
+ const existing = OPEN_DATABASES.get(workspaceName);
439
+ if (existing) {
440
+ return existing;
441
+ }
442
+
443
+ const pending = createWorkspaceDatabase(paths.dbDir).catch((error) => {
444
+ OPEN_DATABASES.delete(workspaceName);
445
+ throw error;
446
+ });
447
+
448
+ OPEN_DATABASES.set(workspaceName, pending);
449
+ return pending;
450
+ }
451
+
452
+ export async function closeOpenDatabases(): Promise<void> {
453
+ const settled = await Promise.allSettled([...OPEN_DATABASES.values()]);
454
+ OPEN_DATABASES.clear();
455
+
456
+ await Promise.allSettled(
457
+ settled
458
+ .filter((entry): entry is PromiseFulfilledResult<PGlite> => entry.status === "fulfilled")
459
+ .map(async (entry) => {
460
+ const close = REAL_CLOSE.get(entry.value);
461
+ if (close) {
462
+ await close();
463
+ }
464
+ }),
465
+ );
466
+ }
467
+
468
+ export function parseLimit(limitValue: string): number {
469
+ const parsed = Number.parseInt(limitValue, 10);
470
+ if (!Number.isInteger(parsed) || parsed <= 0) {
471
+ throw new Error(`Invalid --limit value: ${limitValue}. Use a positive integer.`);
472
+ }
473
+ return parsed;
474
+ }
475
+
476
+ export function validateWorkspaceName(name: string): void {
477
+ if (typeof name !== "string") {
478
+ throw new PragmaError("INVALID_WORKSPACE_NAME", 400, "Workspace name must be a string.");
479
+ }
480
+
481
+ if (name.trim().length === 0) {
482
+ throw new PragmaError("INVALID_WORKSPACE_NAME", 400, "Workspace name is required.");
483
+ }
484
+
485
+ if (name.includes("\0") || name.includes("/") || name.includes("\\") || name.includes("..")) {
486
+ throw new PragmaError(
487
+ "INVALID_WORKSPACE_NAME",
488
+ 400,
489
+ "Workspace name cannot contain '/', '\\', '..', or NUL.",
490
+ );
491
+ }
492
+
493
+ if (name === "active_workspace" || RESERVED_ROOT_NAMES.has(name)) {
494
+ throw new PragmaError(
495
+ "INVALID_WORKSPACE_NAME",
496
+ 400,
497
+ "Workspace name is reserved.",
498
+ );
499
+ }
500
+ }
501
+
502
+ async function createWorkspaceDatabase(dbDir: string): Promise<PGlite> {
503
+ const db = new PGlite(dbDir);
504
+ await db.waitReady;
505
+ await ensureRequiredSchema(db);
506
+ await ensureDefaultAgents(db);
507
+ await ensureDefaultHuman(db);
508
+ await ensureConversationSchema(db);
509
+ return patchDatabaseClose(db);
510
+ }
511
+
512
+ function patchDatabaseClose(db: PGlite): PGlite {
513
+ if (REAL_CLOSE.has(db)) {
514
+ return db;
515
+ }
516
+
517
+ const realClose = db.close.bind(db);
518
+ REAL_CLOSE.set(db, realClose);
519
+ db.close = async () => {};
520
+ return db;
521
+ }
522
+
523
+ async function ensureRequiredSchema(db: PGlite): Promise<void> {
524
+ await ensureTaskStatusEnumType(db);
525
+
526
+ await db.exec(`
527
+ CREATE TABLE IF NOT EXISTS agents (
528
+ id VARCHAR(64) PRIMARY KEY,
529
+ name VARCHAR(255) NOT NULL,
530
+ description TEXT,
531
+ status VARCHAR(32) NOT NULL DEFAULT 'idle',
532
+ agent_file TEXT,
533
+ emoji VARCHAR(32),
534
+ harness VARCHAR(32) NOT NULL DEFAULT 'claude_code',
535
+ model_label VARCHAR(128) NOT NULL DEFAULT 'Opus 4.6',
536
+ model_id VARCHAR(128) NOT NULL DEFAULT 'opus'
537
+ );
538
+ `);
539
+
540
+ await db.exec(`
541
+ ALTER TABLE agents
542
+ ADD COLUMN IF NOT EXISTS description TEXT
543
+ `);
544
+
545
+ await db.exec(`
546
+ CREATE TABLE IF NOT EXISTS tasks (
547
+ id VARCHAR(64) PRIMARY KEY,
548
+ title TEXT NOT NULL,
549
+ status task_status NOT NULL DEFAULT 'queued',
550
+ assigned_to VARCHAR(64),
551
+ output_dir TEXT,
552
+ session_id VARCHAR(255),
553
+ git_branch_name VARCHAR(255),
554
+ git_state_json TEXT,
555
+ test_commands_json TEXT,
556
+ merge_retry_count INTEGER NOT NULL DEFAULT 0,
557
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
558
+ completed_at TIMESTAMPTZ,
559
+ FOREIGN KEY (assigned_to) REFERENCES agents(id)
560
+ );
561
+ `);
562
+ const statusColumn = await db.query<{ udt_name: string }>(
563
+ `
564
+ SELECT udt_name
565
+ FROM information_schema.columns
566
+ WHERE table_schema = 'public'
567
+ AND table_name = 'tasks'
568
+ AND column_name = 'status'
569
+ LIMIT 1
570
+ `,
571
+ );
572
+
573
+ const statusType = statusColumn.rows[0]?.udt_name ?? "";
574
+ if (statusType && statusType !== "task_status") {
575
+ await db.exec(`
576
+ ALTER TABLE tasks
577
+ ALTER COLUMN status TYPE task_status
578
+ USING status::task_status
579
+ `);
580
+ }
581
+
582
+ await db.exec(`
583
+ ALTER TABLE tasks
584
+ ALTER COLUMN status SET DEFAULT 'queued'
585
+ `);
586
+
587
+ await db.exec(`
588
+ ALTER TABLE tasks
589
+ ALTER COLUMN status SET NOT NULL
590
+ `);
591
+
592
+ await db.exec(`
593
+ ALTER TABLE tasks
594
+ ADD COLUMN IF NOT EXISTS git_branch_name VARCHAR(255)
595
+ `);
596
+
597
+ await db.exec(`
598
+ ALTER TABLE tasks
599
+ ADD COLUMN IF NOT EXISTS git_state_json TEXT
600
+ `);
601
+
602
+ await db.exec(`
603
+ ALTER TABLE tasks
604
+ ADD COLUMN IF NOT EXISTS test_commands_json TEXT
605
+ `);
606
+
607
+ await db.exec(`
608
+ ALTER TABLE tasks
609
+ ADD COLUMN IF NOT EXISTS merge_retry_count INTEGER NOT NULL DEFAULT 0
610
+ `);
611
+
612
+ await db.exec(`
613
+ ALTER TABLE tasks
614
+ ADD COLUMN IF NOT EXISTS completed_at TIMESTAMPTZ
615
+ `);
616
+
617
+ await db.exec(`
618
+ ALTER TABLE tasks
619
+ ADD COLUMN IF NOT EXISTS plan TEXT
620
+ `);
621
+
622
+ await db.exec(`
623
+ ALTER TABLE tasks
624
+ ADD COLUMN IF NOT EXISTS followup_task_id VARCHAR(64)
625
+ `);
626
+
627
+ await db.exec(`
628
+ ALTER TABLE tasks
629
+ ADD COLUMN IF NOT EXISTS predecessor_task_id VARCHAR(64)
630
+ `);
631
+
632
+ await db.exec(`
633
+ ALTER TABLE tasks
634
+ ADD COLUMN IF NOT EXISTS push_after_merge BOOLEAN NOT NULL DEFAULT FALSE
635
+ `);
636
+
637
+ await db.exec(`
638
+ ALTER TABLE tasks
639
+ ADD COLUMN IF NOT EXISTS changes_diff TEXT
640
+ `);
641
+
642
+ await db.exec(`
643
+ ALTER TABLE tasks
644
+ ADD COLUMN IF NOT EXISTS testing_config_json TEXT
645
+ `);
646
+
647
+ await db.exec(`
648
+ CREATE TABLE IF NOT EXISTS humans (
649
+ id VARCHAR(64) PRIMARY KEY DEFAULT gen_random_uuid()::VARCHAR(64),
650
+ emoji VARCHAR(32) NOT NULL,
651
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
652
+ );
653
+ `);
654
+
655
+ await db.exec(`
656
+ CREATE TABLE IF NOT EXISTS skills (
657
+ id VARCHAR(64) PRIMARY KEY,
658
+ name VARCHAR(255) UNIQUE NOT NULL,
659
+ description TEXT,
660
+ content TEXT NOT NULL
661
+ );
662
+ `);
663
+
664
+ await db.exec(`
665
+ CREATE TABLE IF NOT EXISTS agent_skills (
666
+ agent_id VARCHAR(64) REFERENCES agents(id) ON DELETE CASCADE,
667
+ skill_id VARCHAR(64) REFERENCES skills(id) ON DELETE CASCADE,
668
+ PRIMARY KEY (agent_id, skill_id)
669
+ );
670
+ `);
671
+
672
+ await db.exec(`
673
+ CREATE TABLE IF NOT EXISTS connectors (
674
+ id VARCHAR(64) PRIMARY KEY,
675
+ name VARCHAR(255) UNIQUE NOT NULL,
676
+ display_name VARCHAR(255),
677
+ description TEXT,
678
+ content TEXT NOT NULL,
679
+ provider VARCHAR(64) NOT NULL,
680
+ binary_name VARCHAR(64) NOT NULL,
681
+ env_var VARCHAR(128) NOT NULL,
682
+ auth_type VARCHAR(32) NOT NULL DEFAULT 'oauth2',
683
+ oauth_client_id TEXT,
684
+ oauth_client_secret TEXT,
685
+ oauth_auth_url TEXT NOT NULL,
686
+ oauth_token_url TEXT NOT NULL,
687
+ scopes TEXT NOT NULL DEFAULT '',
688
+ redirect_uri TEXT NOT NULL DEFAULT 'http://127.0.0.1:3000/connectors/callback',
689
+ status VARCHAR(32) NOT NULL DEFAULT 'disconnected',
690
+ access_token TEXT,
691
+ refresh_token TEXT,
692
+ token_expires_at TIMESTAMPTZ
693
+ );
694
+ `);
695
+
696
+ await db.exec(`
697
+ CREATE TABLE IF NOT EXISTS agent_connectors (
698
+ agent_id VARCHAR(64) REFERENCES agents(id) ON DELETE CASCADE,
699
+ connector_id VARCHAR(64) REFERENCES connectors(id) ON DELETE CASCADE,
700
+ PRIMARY KEY (agent_id, connector_id)
701
+ );
702
+ `);
703
+ }
704
+
705
+ async function ensureTaskStatusEnumType(db: PGlite): Promise<void> {
706
+ const statusType = await db.query<{ exists: boolean }>(
707
+ `
708
+ SELECT EXISTS (
709
+ SELECT 1
710
+ FROM pg_type t
711
+ JOIN pg_namespace n ON n.oid = t.typnamespace
712
+ WHERE t.typname = 'task_status'
713
+ AND n.nspname = 'public'
714
+ ) AS exists
715
+ `,
716
+ );
717
+ if (!statusType.rows[0]?.exists) {
718
+ await db.exec(`
719
+ CREATE TYPE task_status AS ENUM (
720
+ 'queued',
721
+ 'orchestrating',
722
+ 'running',
723
+ 'waiting_for_recipient',
724
+ 'waiting_for_question_response',
725
+ 'waiting_for_help_response',
726
+ 'pending_review',
727
+ 'needs_fix',
728
+ 'completed',
729
+ 'failed',
730
+ 'cancelled'
731
+ );
732
+ `);
733
+ }
734
+
735
+ await db.exec(`ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'planning'`);
736
+ await db.exec(`ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'planned'`);
737
+ await db.exec(`ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'merging'`);
738
+ }
739
+
740
+ async function ensureDefaultAgents(db: PGlite, orchestratorHarness?: string): Promise<void> {
741
+ const seeds = orchestratorHarness
742
+ ? DEFAULT_AGENT_SEEDS.map((seed) => {
743
+ const harness = orchestratorHarness;
744
+ const defaultModel = getDefaultModelForHarness(harness);
745
+ return {
746
+ ...seed,
747
+ harness,
748
+ model_label: defaultModel.label,
749
+ model_id: defaultModel.id,
750
+ };
751
+ })
752
+ : DEFAULT_AGENT_SEEDS;
753
+
754
+ const orchestrator = seeds[0];
755
+ await db.query(
756
+ `
757
+ INSERT INTO agents (id, name, description, status, agent_file, emoji, harness, model_label, model_id)
758
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
759
+ ON CONFLICT (id) DO UPDATE
760
+ SET name = EXCLUDED.name,
761
+ description = EXCLUDED.description,
762
+ status = EXCLUDED.status,
763
+ agent_file = EXCLUDED.agent_file,
764
+ emoji = EXCLUDED.emoji,
765
+ harness = EXCLUDED.harness,
766
+ model_label = EXCLUDED.model_label,
767
+ model_id = EXCLUDED.model_id
768
+ `,
769
+ [
770
+ orchestrator.id,
771
+ orchestrator.name,
772
+ orchestrator.description,
773
+ orchestrator.status,
774
+ orchestrator.agent_file,
775
+ orchestrator.emoji,
776
+ orchestrator.harness,
777
+ orchestrator.model_label,
778
+ orchestrator.model_id,
779
+ ],
780
+ );
781
+
782
+ for (const agent of seeds.slice(1)) {
783
+ await db.query(
784
+ `
785
+ INSERT INTO agents (id, name, description, status, agent_file, emoji, harness, model_label, model_id)
786
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
787
+ ON CONFLICT (id) DO NOTHING
788
+ `,
789
+ [
790
+ agent.id,
791
+ agent.name,
792
+ agent.description,
793
+ agent.status,
794
+ agent.agent_file,
795
+ agent.emoji,
796
+ agent.harness,
797
+ agent.model_label,
798
+ agent.model_id,
799
+ ],
800
+ );
801
+ }
802
+ }
803
+
804
+ const DEFAULT_HUMAN_ID = "you";
805
+ const DEFAULT_HUMAN_EMOJI = "🌿";
806
+
807
+ async function ensureDefaultHuman(db: PGlite): Promise<void> {
808
+ await db.query(
809
+ `INSERT INTO humans (id, emoji) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING`,
810
+ [DEFAULT_HUMAN_ID, DEFAULT_HUMAN_EMOJI],
811
+ );
812
+ }
813
+
814
+ async function pathExists(path: string): Promise<boolean> {
815
+ try {
816
+ await stat(path);
817
+ return true;
818
+ } catch {
819
+ return false;
820
+ }
821
+ }
822
+
823
+ export async function updateTaskTitle(
824
+ db: PGlite,
825
+ taskId: string,
826
+ title: string,
827
+ ): Promise<void> {
828
+ await db.query(`UPDATE tasks SET title = $2 WHERE id = $1`, [taskId, title]);
829
+ }
830
+
831
+ async function clearActiveWorkspaceName(): Promise<void> {
832
+ try {
833
+ await unlink(ACTIVE_WORKSPACE_FILE);
834
+ } catch {
835
+ // No active workspace file to remove.
836
+ }
837
+ }