@grackle-ai/server 0.39.1 → 0.41.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/dist/db.d.ts.map +1 -1
- package/dist/db.js +62 -0
- package/dist/db.js.map +1 -1
- package/dist/env-registry.d.ts +1 -1
- package/dist/env-registry.d.ts.map +1 -1
- package/dist/env-registry.js +1 -2
- package/dist/env-registry.js.map +1 -1
- package/dist/event-bus.d.ts +37 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +65 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/event-processor.d.ts.map +1 -1
- package/dist/event-processor.js +14 -11
- package/dist/event-processor.js.map +1 -1
- package/dist/event-store.d.ts +9 -0
- package/dist/event-store.d.ts.map +1 -0
- package/dist/event-store.js +16 -0
- package/dist/event-store.js.map +1 -0
- package/dist/github-import.js +3 -5
- package/dist/github-import.js.map +1 -1
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +106 -129
- package/dist/grpc-service.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/dist/project-store.d.ts +3 -1
- package/dist/project-store.d.ts.map +1 -1
- package/dist/project-store.js +5 -1
- package/dist/project-store.js.map +1 -1
- package/dist/reanimate-agent.d.ts +12 -0
- package/dist/reanimate-agent.d.ts.map +1 -0
- package/dist/reanimate-agent.js +78 -0
- package/dist/reanimate-agent.js.map +1 -0
- package/dist/resolve-persona.d.ts +29 -0
- package/dist/resolve-persona.d.ts.map +1 -0
- package/dist/resolve-persona.js +40 -0
- package/dist/resolve-persona.js.map +1 -0
- package/dist/schema.d.ts +123 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +9 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-store.d.ts +6 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +23 -5
- package/dist/session-store.js.map +1 -1
- package/dist/settings-store.d.ts +14 -0
- package/dist/settings-store.d.ts.map +1 -0
- package/dist/settings-store.js +29 -0
- package/dist/settings-store.js.map +1 -0
- package/dist/task-store.d.ts +2 -2
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +10 -5
- package/dist/task-store.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +163 -164
- package/dist/ws-bridge.js.map +1 -1
- package/dist/ws-broadcast.d.ts +5 -0
- package/dist/ws-broadcast.d.ts.map +1 -1
- package/dist/ws-broadcast.js +24 -1
- package/dist/ws-broadcast.js.map +1 -1
- package/package.json +7 -6
package/dist/grpc-service.js
CHANGED
|
@@ -12,17 +12,21 @@ import * as projectStore from "./project-store.js";
|
|
|
12
12
|
import * as taskStore from "./task-store.js";
|
|
13
13
|
import * as findingStore from "./finding-store.js";
|
|
14
14
|
import * as personaStore from "./persona-store.js";
|
|
15
|
-
import {
|
|
15
|
+
import { emit } from "./event-bus.js";
|
|
16
16
|
import { processEventStream } from "./event-processor.js";
|
|
17
17
|
import * as processorRegistry from "./processor-registry.js";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
-
import { LOGS_DIR,
|
|
19
|
+
import { LOGS_DIR, DEFAULT_WEB_PORT, DEFAULT_MCP_PORT, MAX_TASK_DEPTH, SESSION_STATUS, TASK_STATUS, taskStatusToEnum, taskStatusToString, projectStatusToEnum, claudeProviderModeToEnum, providerToggleToEnum, } from "@grackle-ai/common";
|
|
20
|
+
import { resolvePersona } from "./resolve-persona.js";
|
|
21
|
+
import * as settingsStore from "./settings-store.js";
|
|
22
|
+
import { isAllowedSettingKey } from "./settings-store.js";
|
|
20
23
|
import { createScopedToken } from "@grackle-ai/mcp";
|
|
21
24
|
import { grackleHome } from "./paths.js";
|
|
22
25
|
import { safeParseJsonArray } from "./json-helpers.js";
|
|
23
26
|
import { computeTaskStatus } from "./compute-task-status.js";
|
|
24
27
|
import { loadOrCreateApiKey } from "./api-key.js";
|
|
25
28
|
import { logger } from "./logger.js";
|
|
29
|
+
import { reanimateAgent } from "./reanimate-agent.js";
|
|
26
30
|
import { slugify } from "./utils/slugify.js";
|
|
27
31
|
import { buildTaskSystemContext } from "./utils/system-context.js";
|
|
28
32
|
import { importGitHubIssues as executeGitHubImport } from "./github-import.js";
|
|
@@ -50,7 +54,6 @@ function envRowToProto(row) {
|
|
|
50
54
|
displayName: row.displayName,
|
|
51
55
|
adapterType: row.adapterType,
|
|
52
56
|
adapterConfig: row.adapterConfig,
|
|
53
|
-
defaultRuntime: row.defaultRuntime,
|
|
54
57
|
bootstrapped: row.bootstrapped,
|
|
55
58
|
status: row.status,
|
|
56
59
|
lastSeen: row.lastSeen || "",
|
|
@@ -89,6 +92,7 @@ function projectRowToProto(row) {
|
|
|
89
92
|
updatedAt: row.updatedAt,
|
|
90
93
|
useWorktrees: row.useWorktrees,
|
|
91
94
|
worktreeBasePath: row.worktreeBasePath,
|
|
95
|
+
defaultPersonaId: row.defaultPersonaId,
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
function taskRowToProto(row, childIds, computedStatus, latestSessionId) {
|
|
@@ -110,6 +114,7 @@ function taskRowToProto(row, childIds, computedStatus, latestSessionId) {
|
|
|
110
114
|
depth: row.depth,
|
|
111
115
|
childTaskIds: childIds ?? taskStore.getChildren(row.id).map((c) => c.id),
|
|
112
116
|
canDecompose: row.canDecompose,
|
|
117
|
+
defaultPersonaId: row.defaultPersonaId,
|
|
113
118
|
});
|
|
114
119
|
}
|
|
115
120
|
function findingRowToProto(row) {
|
|
@@ -173,8 +178,15 @@ function personaRowToProto(row) {
|
|
|
173
178
|
}
|
|
174
179
|
/** Convert persona MCP server configs to a JSON string for the PowerLine SpawnRequest. */
|
|
175
180
|
function personaMcpServersToJson(row) {
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
let mcpServers;
|
|
182
|
+
try {
|
|
183
|
+
mcpServers = JSON.parse(row.mcpServers || "[]");
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
logger.warn({ personaId: row.id }, "Failed to parse persona mcpServers JSON; ignoring");
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(mcpServers) || mcpServers.length === 0) {
|
|
178
190
|
return "";
|
|
179
191
|
}
|
|
180
192
|
return buildMcpServersJson(mcpServers);
|
|
@@ -202,9 +214,8 @@ export function registerGrackleRoutes(router) {
|
|
|
202
214
|
},
|
|
203
215
|
async addEnvironment(req) {
|
|
204
216
|
const id = req.displayName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
broadcastEnvironments();
|
|
217
|
+
envRegistry.addEnvironment(id, req.displayName, req.adapterType, req.adapterConfig);
|
|
218
|
+
emit("environment.changed", {});
|
|
208
219
|
const row = envRegistry.getEnvironment(id);
|
|
209
220
|
return envRowToProto(row);
|
|
210
221
|
},
|
|
@@ -226,11 +237,8 @@ export function registerGrackleRoutes(router) {
|
|
|
226
237
|
// Delete sessions referencing this environment (FK constraint)
|
|
227
238
|
sessionStore.deleteByEnvironment(req.id);
|
|
228
239
|
envRegistry.removeEnvironment(req.id);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
type: "environment_removed",
|
|
232
|
-
payload: { environmentId: req.id },
|
|
233
|
-
});
|
|
240
|
+
emit("environment.changed", {});
|
|
241
|
+
emit("environment.removed", { environmentId: req.id });
|
|
234
242
|
return create(grackle.EmptySchema, {});
|
|
235
243
|
},
|
|
236
244
|
async *provisionEnvironment(req) {
|
|
@@ -253,7 +261,7 @@ export function registerGrackleRoutes(router) {
|
|
|
253
261
|
return;
|
|
254
262
|
}
|
|
255
263
|
envRegistry.updateEnvironmentStatus(req.id, "connecting");
|
|
256
|
-
|
|
264
|
+
emit("environment.changed", {});
|
|
257
265
|
const config = JSON.parse(env.adapterConfig);
|
|
258
266
|
const powerlineToken = env.powerlineToken;
|
|
259
267
|
try {
|
|
@@ -268,7 +276,7 @@ export function registerGrackleRoutes(router) {
|
|
|
268
276
|
catch (err) {
|
|
269
277
|
logger.error({ environmentId: req.id, err }, "Provision/bootstrap failed");
|
|
270
278
|
envRegistry.updateEnvironmentStatus(req.id, "error");
|
|
271
|
-
|
|
279
|
+
emit("environment.changed", {});
|
|
272
280
|
yield create(grackle.ProvisionEventSchema, {
|
|
273
281
|
stage: "error",
|
|
274
282
|
message: `Provision failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -283,7 +291,7 @@ export function registerGrackleRoutes(router) {
|
|
|
283
291
|
await tokenBroker.pushToEnv(req.id);
|
|
284
292
|
envRegistry.updateEnvironmentStatus(req.id, "connected");
|
|
285
293
|
envRegistry.markBootstrapped(req.id);
|
|
286
|
-
|
|
294
|
+
emit("environment.changed", {});
|
|
287
295
|
yield create(grackle.ProvisionEventSchema, {
|
|
288
296
|
stage: "ready",
|
|
289
297
|
message: "Environment connected",
|
|
@@ -292,7 +300,7 @@ export function registerGrackleRoutes(router) {
|
|
|
292
300
|
}
|
|
293
301
|
catch (err) {
|
|
294
302
|
envRegistry.updateEnvironmentStatus(req.id, "error");
|
|
295
|
-
|
|
303
|
+
emit("environment.changed", {});
|
|
296
304
|
yield create(grackle.ProvisionEventSchema, {
|
|
297
305
|
stage: "error",
|
|
298
306
|
message: `Connection failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -311,7 +319,7 @@ export function registerGrackleRoutes(router) {
|
|
|
311
319
|
}
|
|
312
320
|
adapterManager.removeConnection(req.id);
|
|
313
321
|
envRegistry.updateEnvironmentStatus(req.id, "disconnected");
|
|
314
|
-
|
|
322
|
+
emit("environment.changed", {});
|
|
315
323
|
return create(grackle.EmptySchema, {});
|
|
316
324
|
},
|
|
317
325
|
async destroyEnvironment(req) {
|
|
@@ -325,7 +333,7 @@ export function registerGrackleRoutes(router) {
|
|
|
325
333
|
}
|
|
326
334
|
adapterManager.removeConnection(req.id);
|
|
327
335
|
envRegistry.updateEnvironmentStatus(req.id, "disconnected");
|
|
328
|
-
|
|
336
|
+
emit("environment.changed", {});
|
|
329
337
|
return create(grackle.EmptySchema, {});
|
|
330
338
|
},
|
|
331
339
|
async spawnAgent(req) {
|
|
@@ -337,37 +345,35 @@ export function registerGrackleRoutes(router) {
|
|
|
337
345
|
if (!conn) {
|
|
338
346
|
throw new ConnectError(`Environment ${req.environmentId} not connected`, Code.FailedPrecondition);
|
|
339
347
|
}
|
|
340
|
-
// Resolve persona
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
348
|
+
// Resolve persona via cascade (request → app default)
|
|
349
|
+
let resolved;
|
|
350
|
+
try {
|
|
351
|
+
resolved = resolvePersona(req.personaId);
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
throw new ConnectError(err.message, Code.FailedPrecondition);
|
|
346
355
|
}
|
|
347
356
|
const sessionId = uuid();
|
|
348
|
-
const runtime
|
|
349
|
-
const
|
|
350
|
-
persona?.model ||
|
|
351
|
-
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
352
|
-
DEFAULT_MODEL;
|
|
357
|
+
const { runtime, model, systemPrompt, persona } = resolved;
|
|
358
|
+
const maxTurns = req.maxTurns || resolved.maxTurns;
|
|
353
359
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
354
360
|
let systemContext = req.systemContext || "";
|
|
355
|
-
if (
|
|
361
|
+
if (systemPrompt) {
|
|
356
362
|
systemContext =
|
|
357
|
-
|
|
363
|
+
systemPrompt + (systemContext ? "\n\n" + systemContext : "");
|
|
358
364
|
}
|
|
359
365
|
sessionStore.createSession(sessionId, req.environmentId, runtime, req.prompt, model, logPath);
|
|
360
|
-
const mcpServersJson =
|
|
366
|
+
const mcpServersJson = personaMcpServersToJson(persona);
|
|
361
367
|
const mcpPort = parseInt(process.env.GRACKLE_MCP_PORT || String(DEFAULT_MCP_PORT), 10);
|
|
362
368
|
const mcpDialHost = toDialableHost(process.env.GRACKLE_HOST || "127.0.0.1");
|
|
363
369
|
const mcpUrl = `http://${mcpDialHost}:${mcpPort}/mcp`;
|
|
364
|
-
const mcpToken = createScopedToken({ sub: sessionId, pid: "", per:
|
|
370
|
+
const mcpToken = createScopedToken({ sub: sessionId, pid: "", per: resolved.personaId, sid: sessionId }, loadOrCreateApiKey());
|
|
365
371
|
const powerlineReq = create(powerline.SpawnRequestSchema, {
|
|
366
372
|
sessionId,
|
|
367
373
|
runtime,
|
|
368
374
|
prompt: req.prompt,
|
|
369
375
|
model,
|
|
370
|
-
maxTurns
|
|
376
|
+
maxTurns,
|
|
371
377
|
branch: req.branch,
|
|
372
378
|
worktreeBasePath: req.branch
|
|
373
379
|
? (req.worktreeBasePath.trim() || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
@@ -388,37 +394,7 @@ export function registerGrackleRoutes(router) {
|
|
|
388
394
|
return sessionRowToProto(row);
|
|
389
395
|
},
|
|
390
396
|
async resumeAgent(req) {
|
|
391
|
-
const
|
|
392
|
-
if (!session) {
|
|
393
|
-
throw new ConnectError(`Session not found: ${req.sessionId}`, Code.NotFound);
|
|
394
|
-
}
|
|
395
|
-
const conn = adapterManager.getConnection(session.environmentId);
|
|
396
|
-
if (!conn) {
|
|
397
|
-
throw new ConnectError(`Environment ${session.environmentId} not connected`, Code.FailedPrecondition);
|
|
398
|
-
}
|
|
399
|
-
const powerlineReq = create(powerline.ResumeRequestSchema, {
|
|
400
|
-
sessionId: session.id,
|
|
401
|
-
runtimeSessionId: session.runtimeSessionId || "",
|
|
402
|
-
runtime: session.runtime,
|
|
403
|
-
});
|
|
404
|
-
const logPath = session.logPath || join(grackleHome, LOGS_DIR, session.id);
|
|
405
|
-
// Auto-bind task context from DB if session was previously associated with a task
|
|
406
|
-
let resumeProjectId;
|
|
407
|
-
let resumeTaskId;
|
|
408
|
-
if (session.taskId) {
|
|
409
|
-
const task = taskStore.getTask(session.taskId);
|
|
410
|
-
if (task) {
|
|
411
|
-
resumeProjectId = task.projectId;
|
|
412
|
-
resumeTaskId = task.id;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
processEventStream(conn.client.resume(powerlineReq), {
|
|
416
|
-
sessionId: session.id,
|
|
417
|
-
logPath,
|
|
418
|
-
projectId: resumeProjectId,
|
|
419
|
-
taskId: resumeTaskId,
|
|
420
|
-
});
|
|
421
|
-
const row = sessionStore.getSession(session.id);
|
|
397
|
+
const row = reanimateAgent(req.sessionId);
|
|
422
398
|
return sessionRowToProto(row);
|
|
423
399
|
},
|
|
424
400
|
async sendInput(req) {
|
|
@@ -465,7 +441,7 @@ export function registerGrackleRoutes(router) {
|
|
|
465
441
|
if (session.taskId) {
|
|
466
442
|
const task = taskStore.getTask(session.taskId);
|
|
467
443
|
if (task) {
|
|
468
|
-
|
|
444
|
+
emit("task.updated", { taskId: task.id, projectId: task.projectId });
|
|
469
445
|
}
|
|
470
446
|
}
|
|
471
447
|
return create(grackle.EmptySchema, {});
|
|
@@ -555,10 +531,7 @@ export function registerGrackleRoutes(router) {
|
|
|
555
531
|
const current = credentialProviders.getCredentialProviders();
|
|
556
532
|
const updated = { ...current, [req.provider]: req.value };
|
|
557
533
|
credentialProviders.setCredentialProviders(updated);
|
|
558
|
-
|
|
559
|
-
type: "credential_providers",
|
|
560
|
-
payload: updated,
|
|
561
|
-
});
|
|
534
|
+
emit("credential.providers_changed", updated);
|
|
562
535
|
return create(grackle.CredentialProviderConfigSchema, {
|
|
563
536
|
claude: claudeProviderModeToEnum(updated.claude),
|
|
564
537
|
github: providerToggleToEnum(updated.github),
|
|
@@ -581,8 +554,8 @@ export function registerGrackleRoutes(router) {
|
|
|
581
554
|
}
|
|
582
555
|
// useWorktrees defaults to true when not specified
|
|
583
556
|
const useWorktrees = req.useWorktrees ?? true;
|
|
584
|
-
projectStore.createProject(id, req.name, req.description, req.repoUrl, req.defaultEnvironmentId, useWorktrees, req.worktreeBasePath ?? "");
|
|
585
|
-
|
|
557
|
+
projectStore.createProject(id, req.name, req.description, req.repoUrl, req.defaultEnvironmentId, useWorktrees, req.worktreeBasePath ?? "", req.defaultPersonaId ?? "");
|
|
558
|
+
emit("project.created", { projectId: id });
|
|
586
559
|
const row = projectStore.getProject(id);
|
|
587
560
|
return projectRowToProto(row);
|
|
588
561
|
},
|
|
@@ -594,7 +567,7 @@ export function registerGrackleRoutes(router) {
|
|
|
594
567
|
},
|
|
595
568
|
async archiveProject(req) {
|
|
596
569
|
projectStore.archiveProject(req.id);
|
|
597
|
-
|
|
570
|
+
emit("project.archived", { projectId: req.id });
|
|
598
571
|
return create(grackle.EmptySchema, {});
|
|
599
572
|
},
|
|
600
573
|
async updateProject(req) {
|
|
@@ -615,11 +588,12 @@ export function registerGrackleRoutes(router) {
|
|
|
615
588
|
defaultEnvironmentId: req.defaultEnvironmentId,
|
|
616
589
|
useWorktrees: req.useWorktrees ?? undefined,
|
|
617
590
|
worktreeBasePath: req.worktreeBasePath,
|
|
591
|
+
defaultPersonaId: req.defaultPersonaId,
|
|
618
592
|
});
|
|
619
593
|
if (!row) {
|
|
620
594
|
throw new ConnectError(`Project not found after update: ${req.id}`, Code.NotFound);
|
|
621
595
|
}
|
|
622
|
-
|
|
596
|
+
emit("project.updated", { projectId: req.id });
|
|
623
597
|
return projectRowToProto(row);
|
|
624
598
|
},
|
|
625
599
|
// ─── Tasks ───────────────────────────────────────────────
|
|
@@ -666,12 +640,9 @@ export function registerGrackleRoutes(router) {
|
|
|
666
640
|
taskStore.createTask(id, req.projectId, req.title, req.description, [...req.dependsOn], slugify(project.name), req.parentTaskId,
|
|
667
641
|
// Default to false (no decomposition rights) unless explicitly granted.
|
|
668
642
|
// Orchestrator/root processes that need fork() must opt in.
|
|
669
|
-
req.canDecompose ?? false);
|
|
643
|
+
req.canDecompose ?? false, req.defaultPersonaId ?? "");
|
|
670
644
|
const row = taskStore.getTask(id);
|
|
671
|
-
|
|
672
|
-
type: "task_created",
|
|
673
|
-
payload: { task: row ? { ...row } : null },
|
|
674
|
-
});
|
|
645
|
+
emit("task.created", { taskId: id, projectId: req.projectId });
|
|
675
646
|
return taskRowToProto(row);
|
|
676
647
|
},
|
|
677
648
|
async getTask(req) {
|
|
@@ -696,7 +667,7 @@ export function registerGrackleRoutes(router) {
|
|
|
696
667
|
}
|
|
697
668
|
taskStore.updateTask(req.id, req.title !== "" ? req.title : existing.title, req.description !== "" ? req.description : existing.description, reqStatus, req.dependsOn.length > 0
|
|
698
669
|
? [...req.dependsOn]
|
|
699
|
-
: safeParseJsonArray(existing.dependsOn));
|
|
670
|
+
: safeParseJsonArray(existing.dependsOn), req.defaultPersonaId);
|
|
700
671
|
// Late-bind: associate an existing session with this task
|
|
701
672
|
if (req.sessionId !== "") {
|
|
702
673
|
const session = sessionStore.getSession(req.sessionId);
|
|
@@ -713,10 +684,7 @@ export function registerGrackleRoutes(router) {
|
|
|
713
684
|
}
|
|
714
685
|
sessionStore.setSessionTask(req.sessionId, req.id);
|
|
715
686
|
processorRegistry.lateBind(req.sessionId, req.id, existing.projectId);
|
|
716
|
-
|
|
717
|
-
type: "task_started",
|
|
718
|
-
payload: { taskId: req.id, sessionId: req.sessionId, projectId: existing.projectId },
|
|
719
|
-
});
|
|
687
|
+
emit("task.started", { taskId: req.id, sessionId: req.sessionId, projectId: existing.projectId });
|
|
720
688
|
}
|
|
721
689
|
const row = taskStore.getTask(req.id);
|
|
722
690
|
const taskSessions = sessionStore.listSessionsForTask(req.id);
|
|
@@ -747,39 +715,28 @@ export function registerGrackleRoutes(router) {
|
|
|
747
715
|
const conn = adapterManager.getConnection(environmentId);
|
|
748
716
|
if (!conn)
|
|
749
717
|
throw new ConnectError(`Environment ${environmentId} not connected`, Code.FailedPrecondition);
|
|
750
|
-
// Resolve persona
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
throw new ConnectError(
|
|
718
|
+
// Resolve persona via cascade (request → task → project → app default)
|
|
719
|
+
let resolved;
|
|
720
|
+
try {
|
|
721
|
+
resolved = resolvePersona(req.personaId, task.defaultPersonaId, project.defaultPersonaId);
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
throw new ConnectError(err.message, Code.FailedPrecondition);
|
|
757
725
|
}
|
|
758
726
|
const env = envRegistry.getEnvironment(environmentId);
|
|
759
727
|
const sessionId = uuid();
|
|
760
|
-
const runtime =
|
|
761
|
-
persona?.runtime ||
|
|
762
|
-
env?.defaultRuntime ||
|
|
763
|
-
DEFAULT_RUNTIME;
|
|
764
|
-
const model = req.model ||
|
|
765
|
-
persona?.model ||
|
|
766
|
-
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
767
|
-
DEFAULT_MODEL;
|
|
768
|
-
const maxTurns = persona?.maxTurns || 0;
|
|
728
|
+
const { runtime, model, maxTurns, systemPrompt, persona } = resolved;
|
|
769
729
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
770
730
|
let systemContext = buildTaskSystemContext(task.title, task.description, req.notes || "", task.canDecompose);
|
|
771
|
-
if (
|
|
772
|
-
systemContext =
|
|
731
|
+
if (systemPrompt) {
|
|
732
|
+
systemContext = systemPrompt + "\n\n" + systemContext;
|
|
773
733
|
}
|
|
774
|
-
sessionStore.createSession(sessionId, environmentId, runtime, task.title, model, logPath, task.id, personaId);
|
|
775
|
-
|
|
776
|
-
type: "task_started",
|
|
777
|
-
payload: { taskId: task.id, sessionId, projectId: task.projectId },
|
|
778
|
-
});
|
|
734
|
+
sessionStore.createSession(sessionId, environmentId, runtime, task.title, model, logPath, task.id, resolved.personaId);
|
|
735
|
+
emit("task.started", { taskId: task.id, sessionId, projectId: task.projectId });
|
|
779
736
|
// Re-push stored tokens + provider credentials (scoped to runtime) so they're fresh for this session.
|
|
780
737
|
// For local envs, skip file tokens — the PowerLine is on the same machine.
|
|
781
738
|
await tokenBroker.refreshTokensForTask(environmentId, runtime, env?.adapterType === "local" ? { excludeFileTokens: true } : undefined);
|
|
782
|
-
const mcpServersJson =
|
|
739
|
+
const mcpServersJson = personaMcpServersToJson(persona);
|
|
783
740
|
// When useWorktrees is false, omit worktreeBasePath so PowerLine checks
|
|
784
741
|
// out the branch in the main working tree instead of creating a worktree.
|
|
785
742
|
// The branch field is still populated so the agent knows its branch name.
|
|
@@ -790,7 +747,7 @@ export function registerGrackleRoutes(router) {
|
|
|
790
747
|
const taskMcpPort = parseInt(process.env.GRACKLE_MCP_PORT || String(DEFAULT_MCP_PORT), 10);
|
|
791
748
|
const taskMcpDialHost = toDialableHost(process.env.GRACKLE_HOST || "127.0.0.1");
|
|
792
749
|
const taskMcpUrl = `http://${taskMcpDialHost}:${taskMcpPort}/mcp`;
|
|
793
|
-
const taskMcpToken = createScopedToken({ sub: task.id, pid: task.projectId, per: personaId, sid: sessionId }, loadOrCreateApiKey());
|
|
750
|
+
const taskMcpToken = createScopedToken({ sub: task.id, pid: task.projectId, per: resolved.personaId, sid: sessionId }, loadOrCreateApiKey());
|
|
794
751
|
const powerlineReq = create(powerline.SpawnRequestSchema, {
|
|
795
752
|
sessionId,
|
|
796
753
|
runtime,
|
|
@@ -837,10 +794,7 @@ export function registerGrackleRoutes(router) {
|
|
|
837
794
|
raw: "",
|
|
838
795
|
}));
|
|
839
796
|
}
|
|
840
|
-
|
|
841
|
-
type: "task_completed",
|
|
842
|
-
payload: { taskId: task.id, projectId: task.projectId },
|
|
843
|
-
});
|
|
797
|
+
emit("task.completed", { taskId: task.id, projectId: task.projectId });
|
|
844
798
|
const row = taskStore.getTask(task.id);
|
|
845
799
|
const taskSessions = sessionStore.listSessionsForTask(task.id);
|
|
846
800
|
const { status, latestSessionId } = computeTaskStatus(row.status, taskSessions);
|
|
@@ -876,10 +830,7 @@ export function registerGrackleRoutes(router) {
|
|
|
876
830
|
projectId: task.projectId,
|
|
877
831
|
taskId: task.id,
|
|
878
832
|
});
|
|
879
|
-
|
|
880
|
-
type: "task_started",
|
|
881
|
-
payload: { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId },
|
|
882
|
-
});
|
|
833
|
+
emit("task.started", { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId });
|
|
883
834
|
const row = sessionStore.getSession(latestSession.id);
|
|
884
835
|
return sessionRowToProto(row);
|
|
885
836
|
},
|
|
@@ -918,10 +869,7 @@ export function registerGrackleRoutes(router) {
|
|
|
918
869
|
logger.error({ taskId: req.id }, "deleteTask returned 0 changes despite task existing");
|
|
919
870
|
throw new ConnectError(`Failed to delete task ${req.id}: no rows affected`, Code.Internal);
|
|
920
871
|
}
|
|
921
|
-
|
|
922
|
-
type: "task_deleted",
|
|
923
|
-
payload: { taskId: req.id, projectId: task.projectId },
|
|
924
|
-
});
|
|
872
|
+
emit("task.deleted", { taskId: req.id, projectId: task.projectId });
|
|
925
873
|
return create(grackle.EmptySchema, {});
|
|
926
874
|
},
|
|
927
875
|
// ─── Personas ───────────────────────────────────────────────
|
|
@@ -955,7 +903,7 @@ export function registerGrackleRoutes(router) {
|
|
|
955
903
|
tools: [...s.tools],
|
|
956
904
|
})));
|
|
957
905
|
personaStore.createPersona(id, req.name, req.description, req.systemPrompt, toolConfigJson, req.runtime, req.model, req.maxTurns, mcpServersJson);
|
|
958
|
-
|
|
906
|
+
emit("persona.created", { personaId: id });
|
|
959
907
|
const row = personaStore.getPersona(id);
|
|
960
908
|
return personaRowToProto(row);
|
|
961
909
|
},
|
|
@@ -1000,23 +948,52 @@ export function registerGrackleRoutes(router) {
|
|
|
1000
948
|
const model = req.model || existing.model;
|
|
1001
949
|
const maxTurns = req.maxTurns === 0 ? existing.maxTurns : req.maxTurns;
|
|
1002
950
|
personaStore.updatePersona(req.id, name, description, systemPrompt, toolConfigJson, runtime, model, maxTurns, mcpServersJson);
|
|
1003
|
-
|
|
951
|
+
emit("persona.updated", { personaId: req.id });
|
|
1004
952
|
const row = personaStore.getPersona(req.id);
|
|
1005
953
|
return personaRowToProto(row);
|
|
1006
954
|
},
|
|
1007
955
|
async deletePersona(req) {
|
|
1008
956
|
personaStore.deletePersona(req.id);
|
|
1009
|
-
|
|
957
|
+
emit("persona.deleted", { personaId: req.id });
|
|
1010
958
|
return create(grackle.EmptySchema, {});
|
|
1011
959
|
},
|
|
960
|
+
// ─── Settings ─────────────────────────────────────────────
|
|
961
|
+
async getSetting(req) {
|
|
962
|
+
if (!isAllowedSettingKey(req.key)) {
|
|
963
|
+
throw new ConnectError(`Setting key not allowed: ${req.key}`, Code.InvalidArgument);
|
|
964
|
+
}
|
|
965
|
+
const value = settingsStore.getSetting(req.key);
|
|
966
|
+
return create(grackle.SettingResponseSchema, {
|
|
967
|
+
key: req.key,
|
|
968
|
+
value: value ?? "",
|
|
969
|
+
});
|
|
970
|
+
},
|
|
971
|
+
async setSetting(req) {
|
|
972
|
+
if (!isAllowedSettingKey(req.key)) {
|
|
973
|
+
throw new ConnectError(`Setting key not allowed: ${req.key}`, Code.InvalidArgument);
|
|
974
|
+
}
|
|
975
|
+
// Validate persona exists and has required fields when setting default_persona_id
|
|
976
|
+
if (req.key === "default_persona_id" && req.value) {
|
|
977
|
+
const persona = personaStore.getPersona(req.value);
|
|
978
|
+
if (!persona) {
|
|
979
|
+
throw new ConnectError(`Persona not found: ${req.value}`, Code.NotFound);
|
|
980
|
+
}
|
|
981
|
+
if (!persona.runtime || !persona.model) {
|
|
982
|
+
throw new ConnectError(`Persona "${persona.name}" must have runtime and model configured`, Code.FailedPrecondition);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
settingsStore.setSetting(req.key, req.value);
|
|
986
|
+
emit("setting.changed", { key: req.key, value: req.value });
|
|
987
|
+
return create(grackle.SettingResponseSchema, {
|
|
988
|
+
key: req.key,
|
|
989
|
+
value: req.value,
|
|
990
|
+
});
|
|
991
|
+
},
|
|
1012
992
|
// ─── Findings ────────────────────────────────────────────
|
|
1013
993
|
async postFinding(req) {
|
|
1014
994
|
const id = uuid().slice(0, 8);
|
|
1015
995
|
findingStore.postFinding(id, req.projectId, req.taskId, req.sessionId, req.category, req.title, req.content, [...req.tags]);
|
|
1016
|
-
|
|
1017
|
-
type: "finding_posted",
|
|
1018
|
-
payload: { projectId: req.projectId, findingId: id },
|
|
1019
|
-
});
|
|
996
|
+
emit("finding.posted", { projectId: req.projectId, findingId: id });
|
|
1020
997
|
const rows = findingStore.queryFindings(req.projectId);
|
|
1021
998
|
const row = rows.find((r) => r.id === id);
|
|
1022
999
|
return findingRowToProto(row);
|