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.
- package/cli/index.ts +882 -0
- package/index.ts +3 -0
- package/package.json +53 -0
- package/server/connectorBinaries.ts +103 -0
- package/server/connectorRegistry.ts +158 -0
- package/server/conversation/adapterRegistry.ts +53 -0
- package/server/conversation/adapters/claudeAdapter.ts +138 -0
- package/server/conversation/adapters/codexAdapter.ts +142 -0
- package/server/conversation/adapters.ts +224 -0
- package/server/conversation/executeRunner.ts +1191 -0
- package/server/conversation/gitWorkflow.ts +1037 -0
- package/server/conversation/models.ts +23 -0
- package/server/conversation/pragmaCli.ts +34 -0
- package/server/conversation/prompts.ts +335 -0
- package/server/conversation/store.ts +805 -0
- package/server/conversation/titleGenerator.ts +106 -0
- package/server/conversation/turnRunner.ts +365 -0
- package/server/conversation/types.ts +134 -0
- package/server/db.ts +837 -0
- package/server/http/middleware.ts +31 -0
- package/server/http/schemas.ts +430 -0
- package/server/http/validators.ts +38 -0
- package/server/index.ts +6560 -0
- package/server/process/runCommand.ts +142 -0
- package/server/stores/agentStore.ts +167 -0
- package/server/stores/connectorStore.ts +299 -0
- package/server/stores/humanStore.ts +28 -0
- package/server/stores/skillStore.ts +127 -0
- package/server/stores/taskStore.ts +371 -0
- package/shared/net.ts +24 -0
- package/tsconfig.json +14 -0
- package/ui/index.html +14 -0
- package/ui/public/favicon-32.png +0 -0
- package/ui/public/favicon.png +0 -0
- package/ui/src/App.jsx +1338 -0
- package/ui/src/api.js +954 -0
- package/ui/src/components/CodeView.jsx +319 -0
- package/ui/src/components/ConnectionsView.jsx +1004 -0
- package/ui/src/components/ContextView.jsx +315 -0
- package/ui/src/components/ConversationDrawer.jsx +963 -0
- package/ui/src/components/EmptyPane.jsx +20 -0
- package/ui/src/components/FeedView.jsx +773 -0
- package/ui/src/components/FilesView.jsx +257 -0
- package/ui/src/components/InlineChatView.jsx +158 -0
- package/ui/src/components/InputBar.jsx +476 -0
- package/ui/src/components/OnboardingModal.jsx +112 -0
- package/ui/src/components/OutputPanel.jsx +658 -0
- package/ui/src/components/PlanProposalPanel.jsx +177 -0
- package/ui/src/components/RightPanel.jsx +951 -0
- package/ui/src/components/SettingsView.jsx +186 -0
- package/ui/src/components/Sidebar.jsx +247 -0
- package/ui/src/components/TestingPane.jsx +198 -0
- package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
- package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
- package/ui/src/components/testing/TerminalPanel.jsx +104 -0
- package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
- package/ui/src/hooks/useAgents.js +81 -0
- package/ui/src/hooks/useConversation.js +252 -0
- package/ui/src/hooks/useTasks.js +161 -0
- package/ui/src/hooks/useWorkspace.js +259 -0
- package/ui/src/lib/agentIcon.js +10 -0
- package/ui/src/lib/conversationUtils.js +575 -0
- package/ui/src/main.jsx +10 -0
- package/ui/src/styles.css +6899 -0
- 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
|
+
}
|