@grackle-ai/server 0.26.0 → 0.28.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/adapter-manager.d.ts.map +1 -1
- package/dist/adapter-manager.js +1 -0
- package/dist/adapter-manager.js.map +1 -1
- package/dist/adapters/codespace.js +1 -1
- package/dist/adapters/codespace.js.map +1 -1
- package/dist/adapters/docker.js +1 -1
- package/dist/adapters/docker.js.map +1 -1
- package/dist/adapters/local.js +1 -1
- package/dist/adapters/local.js.map +1 -1
- package/dist/adapters/remote-adapter-utils.d.ts.map +1 -1
- package/dist/adapters/remote-adapter-utils.js +22 -32
- package/dist/adapters/remote-adapter-utils.js.map +1 -1
- package/dist/adapters/ssh.js +1 -1
- package/dist/adapters/ssh.js.map +1 -1
- package/dist/compute-task-status.d.ts +11 -7
- package/dist/compute-task-status.d.ts.map +1 -1
- package/dist/compute-task-status.js +35 -40
- package/dist/compute-task-status.js.map +1 -1
- package/dist/credential-providers.d.ts +27 -0
- package/dist/credential-providers.d.ts.map +1 -0
- package/dist/credential-providers.js +166 -0
- package/dist/credential-providers.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +55 -5
- package/dist/db.js.map +1 -1
- package/dist/event-processor.d.ts.map +1 -1
- package/dist/event-processor.js +15 -14
- package/dist/event-processor.js.map +1 -1
- package/dist/github-import.d.ts.map +1 -1
- package/dist/github-import.js +3 -2
- package/dist/github-import.js.map +1 -1
- package/dist/grpc-service.d.ts.map +1 -1
- package/dist/grpc-service.js +73 -41
- package/dist/grpc-service.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -0
- 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/schema.d.ts +66 -38
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -3
- package/dist/schema.js.map +1 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +4 -3
- package/dist/session-store.js.map +1 -1
- package/dist/task-store.d.ts +6 -6
- package/dist/task-store.d.ts.map +1 -1
- package/dist/task-store.js +11 -11
- package/dist/task-store.js.map +1 -1
- package/dist/token-broker.d.ts +6 -3
- package/dist/token-broker.d.ts.map +1 -1
- package/dist/token-broker.js +11 -26
- package/dist/token-broker.js.map +1 -1
- package/dist/transcript.js.map +1 -1
- package/dist/utils/format-gh-error.d.ts +6 -0
- package/dist/utils/format-gh-error.d.ts.map +1 -0
- package/dist/utils/format-gh-error.js +30 -0
- package/dist/utils/format-gh-error.js.map +1 -0
- package/dist/utils/system-context.d.ts +1 -1
- package/dist/utils/system-context.d.ts.map +1 -1
- package/dist/utils/system-context.js +2 -2
- package/dist/utils/system-context.js.map +1 -1
- package/dist/ws-bridge.d.ts.map +1 -1
- package/dist/ws-bridge.js +123 -105
- 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 +20 -0
- package/dist/ws-broadcast.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-gh-error.d.ts","sourceRoot":"","sources":["../../src/utils/format-gh-error.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CA4BrE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate a raw `gh` CLI error into a user-friendly message.
|
|
3
|
+
* The raw error is still logged server-side for diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
export function formatGhError(err, operation) {
|
|
6
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7
|
+
const rawStderr = err instanceof Error && "stderr" in err
|
|
8
|
+
? err.stderr
|
|
9
|
+
: undefined;
|
|
10
|
+
const stderr = typeof rawStderr === "string" && rawStderr.length > 0
|
|
11
|
+
? rawStderr
|
|
12
|
+
: Buffer.isBuffer(rawStderr) && rawStderr.length > 0
|
|
13
|
+
? rawStderr.toString()
|
|
14
|
+
: "";
|
|
15
|
+
const code = err instanceof Error && "code" in err
|
|
16
|
+
? String(err.code)
|
|
17
|
+
: "";
|
|
18
|
+
if (code === "ENOENT" || message.includes("ENOENT")) {
|
|
19
|
+
return "Could not find the `gh` CLI. Ensure GitHub CLI is installed and available on your system PATH, then restart the Grackle server.";
|
|
20
|
+
}
|
|
21
|
+
if (code === "EACCES" || message.includes("EACCES")) {
|
|
22
|
+
return "`gh` CLI found but not executable. Check file permissions.";
|
|
23
|
+
}
|
|
24
|
+
const combined = `${stderr} ${message}`.toLowerCase();
|
|
25
|
+
if (combined.includes("auth") || combined.includes("login")) {
|
|
26
|
+
return "GitHub CLI is not authenticated. Run `gh auth login` and restart.";
|
|
27
|
+
}
|
|
28
|
+
return `Failed to ${operation}: ${stderr || message}`;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=format-gh-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-gh-error.js","sourceRoot":"","sources":["../../src/utils/format-gh-error.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY,EAAE,SAAiB;IAC3D,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,MAAM,SAAS,GACb,GAAG,YAAY,KAAK,IAAI,QAAQ,IAAI,GAAG;QACrC,CAAC,CAAE,GAAmC,CAAC,MAAM;QAC7C,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,MAAM,GACV,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QACnD,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAClD,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE;YACtB,CAAC,CAAC,EAAE,CAAC;IACX,MAAM,IAAI,GACR,GAAG,YAAY,KAAK,IAAI,MAAM,IAAI,GAAG;QACnC,CAAC,CAAC,MAAM,CAAE,GAAiC,CAAC,IAAI,CAAC;QACjD,CAAC,CAAC,EAAE,CAAC;IAET,IAAI,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,OAAO,iIAAiI,CAAC;IAC3I,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,OAAO,4DAA4D,CAAC;IACtE,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC,WAAW,EAAE,CAAC;IACtD,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5D,OAAO,mEAAmE,CAAC;IAC7E,CAAC;IACD,OAAO,aAAa,SAAS,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC;AACxD,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Build the system context string injected into task-spawned agent sessions. */
|
|
2
|
-
export declare function buildTaskSystemContext(title: string, description: string,
|
|
2
|
+
export declare function buildTaskSystemContext(title: string, description: string, notes: string, canDecompose?: boolean): string;
|
|
3
3
|
//# sourceMappingURL=system-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-context.d.ts","sourceRoot":"","sources":["../../src/utils/system-context.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"system-context.d.ts","sourceRoot":"","sources":["../../src/utils/system-context.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CA+CxH"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Build the system context string injected into task-spawned agent sessions. */
|
|
2
|
-
export function buildTaskSystemContext(title, description,
|
|
2
|
+
export function buildTaskSystemContext(title, description, notes, canDecompose) {
|
|
3
3
|
const sections = [
|
|
4
4
|
`## Task: ${title}`,
|
|
5
5
|
description,
|
|
6
|
-
|
|
6
|
+
notes ? `## Notes (from previous attempt or user feedback)\n${notes}` : "",
|
|
7
7
|
`## Grackle Tools (MCP)`,
|
|
8
8
|
`You have a "grackle" MCP server with tools for coordinating with other agents:`,
|
|
9
9
|
`- **mcp__grackle__post_finding**: Share discoveries (architecture decisions, bugs, patterns) with other agents working on this project. Parameters: title (string), content (string), category (optional: architecture|api|bug|decision|dependency|pattern|general), tags (optional: string[]).`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-context.js","sourceRoot":"","sources":["../../src/utils/system-context.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,UAAU,sBAAsB,CAAC,KAAa,EAAE,WAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"system-context.js","sourceRoot":"","sources":["../../src/utils/system-context.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,UAAU,sBAAsB,CAAC,KAAa,EAAE,WAAmB,EAAE,KAAa,EAAE,YAAsB;IAC9G,MAAM,QAAQ,GAAa;QACzB,YAAY,KAAK,EAAE;QACnB,WAAW;QACX,KAAK,CAAC,CAAC,CAAC,sDAAsD,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE;QAC1E,wBAAwB;QACxB,gFAAgF;QAChF,iSAAiS;QACjS,gJAAgJ;KACjJ,CAAC;IAEF,IAAI,YAAY,EAAE,CAAC;QACjB,QAAQ,CAAC,IAAI,CACX,8qBAA8qB,CAC/qB,CAAC;IACJ,CAAC;IAED,QAAQ,CAAC,IAAI,CACX,yBAAyB,EACzB,oKAAoK,EACpK,EAAE,EACF,+BAA+B,EAC/B,yCAAyC,EACzC,2MAA2M,EAC3M,mEAAmE,EACnE,4DAA4D,EAC5D,2JAA2J,EAC3J,EAAE,EACF,wBAAwB,EACxB,4IAA4I,EAC5I,0GAA0G,EAC1G,qJAAqJ,EACrJ,8CAA8C,EAC9C,gGAAgG,EAChG,EAAE,EACF,kEAAkE,EAClE,8DAA8D,EAC9D,EAAE,EACF,qKAAqK,EACrK,sIAAsI,EACtI,yTAAyT,EACzT,uGAAuG,EACvG,EAAE,EACF,8NAA8N,CAC/N,CAAC;IAEF,OAAO,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/C,CAAC"}
|
package/dist/ws-bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-bridge.d.ts","sourceRoot":"","sources":["../src/ws-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAa,MAAM,IAAI,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"ws-bridge.d.ts","sourceRoot":"","sources":["../src/ws-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAa,MAAM,IAAI,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AAuDtD,wGAAwG;AACxG,wBAAgB,cAAc,CAC5B,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GACvC,eAAe,CAwCjB"}
|
package/dist/ws-bridge.js
CHANGED
|
@@ -7,13 +7,14 @@ import * as adapterManager from "./adapter-manager.js";
|
|
|
7
7
|
import { reconnectOrProvision, } from "./adapters/adapter.js";
|
|
8
8
|
import * as streamHub from "./stream-hub.js";
|
|
9
9
|
import * as tokenBroker from "./token-broker.js";
|
|
10
|
+
import * as credentialProviders from "./credential-providers.js";
|
|
10
11
|
import * as projectStore from "./project-store.js";
|
|
11
12
|
import * as taskStore from "./task-store.js";
|
|
12
13
|
import * as findingStore from "./finding-store.js";
|
|
13
14
|
import * as personaStore from "./persona-store.js";
|
|
14
15
|
import { v4 as uuid } from "uuid";
|
|
15
16
|
import { join } from "node:path";
|
|
16
|
-
import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, eventTypeToString, } from "@grackle-ai/common";
|
|
17
|
+
import { LOGS_DIR, DEFAULT_RUNTIME, DEFAULT_MODEL, SESSION_STATUS, TASK_STATUS, eventTypeToString, } from "@grackle-ai/common";
|
|
17
18
|
import { grackleHome } from "./paths.js";
|
|
18
19
|
import * as logWriter from "./log-writer.js";
|
|
19
20
|
import { safeParseJsonArray } from "./json-helpers.js";
|
|
@@ -22,10 +23,11 @@ import { buildTaskSystemContext } from "./utils/system-context.js";
|
|
|
22
23
|
import { slugify } from "./utils/slugify.js";
|
|
23
24
|
import { processEventStream } from "./event-processor.js";
|
|
24
25
|
import * as processorRegistry from "./processor-registry.js";
|
|
25
|
-
import { broadcast, setWssInstance } from "./ws-broadcast.js";
|
|
26
|
+
import { broadcast, setWssInstance, broadcastEnvironments, envRowToWs } from "./ws-broadcast.js";
|
|
26
27
|
import { buildMcpServersJson } from "./grpc-service.js";
|
|
27
28
|
import { computeTaskStatus } from "./compute-task-status.js";
|
|
28
29
|
import { exec } from "./utils/exec.js";
|
|
30
|
+
import { formatGhError } from "./utils/format-gh-error.js";
|
|
29
31
|
const GH_CODESPACE_LIST_TIMEOUT_MS = 30_000;
|
|
30
32
|
const GH_CODESPACE_CREATE_TIMEOUT_MS = 300_000;
|
|
31
33
|
const GH_CODESPACE_LIST_LIMIT = 50;
|
|
@@ -43,6 +45,7 @@ export function createWsBridge(httpServer, verifyApiKey) {
|
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
const subscriptions = new Map();
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
46
49
|
ws.on("message", async (data) => {
|
|
47
50
|
try {
|
|
48
51
|
const msg = JSON.parse(data.toString());
|
|
@@ -66,25 +69,6 @@ export function createWsBridge(httpServer, verifyApiKey) {
|
|
|
66
69
|
});
|
|
67
70
|
return wss;
|
|
68
71
|
}
|
|
69
|
-
/** Map a database environment row to the WebSocket payload shape. */
|
|
70
|
-
function envRowToWs(r) {
|
|
71
|
-
return {
|
|
72
|
-
id: r.id,
|
|
73
|
-
displayName: r.displayName,
|
|
74
|
-
adapterType: r.adapterType,
|
|
75
|
-
adapterConfig: r.adapterConfig,
|
|
76
|
-
defaultRuntime: r.defaultRuntime,
|
|
77
|
-
status: r.status,
|
|
78
|
-
bootstrapped: r.bootstrapped,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/** Broadcast the current environment list to all connected WebSocket clients. */
|
|
82
|
-
function broadcastEnvironments() {
|
|
83
|
-
broadcast({
|
|
84
|
-
type: "environments",
|
|
85
|
-
payload: { environments: envRegistry.listEnvironments().map(envRowToWs) },
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
72
|
/** Safely parse an adapter config string, returning an empty object on failure. */
|
|
89
73
|
function safeParseAdapterConfig(raw, environmentId) {
|
|
90
74
|
try {
|
|
@@ -213,18 +197,13 @@ async function startTaskSession(ws, task, options) {
|
|
|
213
197
|
return `Persona not found: ${resolvedPersonaId}`;
|
|
214
198
|
}
|
|
215
199
|
const sessionId = uuid();
|
|
216
|
-
const runtime = options?.runtime ||
|
|
217
|
-
|
|
218
|
-
env.
|
|
219
|
-
DEFAULT_RUNTIME;
|
|
220
|
-
const model = options?.model ||
|
|
221
|
-
persona?.model ||
|
|
222
|
-
process.env.GRACKLE_DEFAULT_MODEL ||
|
|
223
|
-
DEFAULT_MODEL;
|
|
200
|
+
const runtime = options?.runtime || persona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
|
|
201
|
+
const model = options?.model || persona?.model ||
|
|
202
|
+
process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
|
|
224
203
|
const maxTurns = persona?.maxTurns || 0;
|
|
225
204
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
226
205
|
const freshTask = taskStore.getTask(task.id) || task;
|
|
227
|
-
let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description,
|
|
206
|
+
let systemContext = buildTaskSystemContext(freshTask.title, freshTask.description, options?.notes || "", freshTask.canDecompose);
|
|
228
207
|
if (persona) {
|
|
229
208
|
systemContext = persona.systemPrompt + "\n\n" + systemContext;
|
|
230
209
|
}
|
|
@@ -262,7 +241,7 @@ async function startTaskSession(ws, task, options) {
|
|
|
262
241
|
maxTurns,
|
|
263
242
|
branch: freshTask.branch,
|
|
264
243
|
worktreeBasePath: freshTask.branch
|
|
265
|
-
? process.env.GRACKLE_WORKTREE_BASE || "/workspace"
|
|
244
|
+
? (project.worktreeBasePath || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
266
245
|
: "",
|
|
267
246
|
systemContext,
|
|
268
247
|
projectId: freshTask.projectId,
|
|
@@ -312,7 +291,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
312
291
|
return;
|
|
313
292
|
}
|
|
314
293
|
const session = sessionStore.getSession(sessionId);
|
|
315
|
-
if (!session
|
|
294
|
+
if (!session?.logPath) {
|
|
316
295
|
return;
|
|
317
296
|
}
|
|
318
297
|
const entries = logWriter.readLog(session.logPath);
|
|
@@ -386,8 +365,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
386
365
|
case "spawn": {
|
|
387
366
|
const environmentId = msg.payload?.environmentId;
|
|
388
367
|
const prompt = msg.payload?.prompt;
|
|
389
|
-
const model = msg.payload?.model ||
|
|
390
|
-
const runtime = msg.payload?.runtime ||
|
|
368
|
+
const model = msg.payload?.model || undefined;
|
|
369
|
+
const runtime = msg.payload?.runtime || undefined;
|
|
391
370
|
const branch = msg.payload?.branch || "";
|
|
392
371
|
const systemContext = msg.payload?.systemContext || "";
|
|
393
372
|
const spawnPersonaId = msg.payload?.personaId || "";
|
|
@@ -418,7 +397,9 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
418
397
|
return;
|
|
419
398
|
}
|
|
420
399
|
const sessionId = uuid();
|
|
400
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime/model may be undefined from WS payload
|
|
421
401
|
const sessionRuntime = runtime || spawnPersona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
|
|
402
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
422
403
|
const sessionModel = model || spawnPersona?.model || process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
|
|
423
404
|
const maxTurns = spawnPersona?.maxTurns || 0;
|
|
424
405
|
const logPath = join(grackleHome, LOGS_DIR, sessionId);
|
|
@@ -435,7 +416,9 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
435
416
|
model: sessionModel,
|
|
436
417
|
maxTurns,
|
|
437
418
|
branch,
|
|
438
|
-
worktreeBasePath: branch
|
|
419
|
+
worktreeBasePath: branch
|
|
420
|
+
? ((typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "") || process.env.GRACKLE_WORKTREE_BASE || "/workspace")
|
|
421
|
+
: "",
|
|
439
422
|
systemContext: finalSystemContext,
|
|
440
423
|
});
|
|
441
424
|
processEventStream(conn.client.spawn(powerlineReq), {
|
|
@@ -448,7 +431,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
448
431
|
sessionId,
|
|
449
432
|
eventType: "error",
|
|
450
433
|
timestamp: new Date().toISOString(),
|
|
451
|
-
content: `Spawn failed: ${err}`,
|
|
434
|
+
content: `Spawn failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
452
435
|
},
|
|
453
436
|
});
|
|
454
437
|
},
|
|
@@ -473,11 +456,11 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
473
456
|
});
|
|
474
457
|
return;
|
|
475
458
|
}
|
|
476
|
-
if (session.status !==
|
|
459
|
+
if (session.status !== SESSION_STATUS.IDLE) {
|
|
477
460
|
sendWs(ws, {
|
|
478
461
|
type: "error",
|
|
479
462
|
payload: {
|
|
480
|
-
message: `Session ${sessionId} is not currently
|
|
463
|
+
message: `Session ${sessionId} is not currently idle (status: ${session.status})`,
|
|
481
464
|
},
|
|
482
465
|
});
|
|
483
466
|
return;
|
|
@@ -530,15 +513,15 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
530
513
|
await conn.client.kill(create(powerline.SessionIdSchema, { id: sessionId }));
|
|
531
514
|
}
|
|
532
515
|
catch (err) {
|
|
533
|
-
logger.warn({ sessionId, err }, "PowerLine kill failed — marking session
|
|
516
|
+
logger.warn({ sessionId, err }, "PowerLine kill failed — marking session interrupted anyway");
|
|
534
517
|
}
|
|
535
518
|
}
|
|
536
|
-
sessionStore.updateSession(sessionId,
|
|
519
|
+
sessionStore.updateSession(sessionId, SESSION_STATUS.INTERRUPTED);
|
|
537
520
|
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
538
521
|
sessionId,
|
|
539
522
|
type: grackle.EventType.STATUS,
|
|
540
523
|
timestamp: new Date().toISOString(),
|
|
541
|
-
content:
|
|
524
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
542
525
|
raw: "",
|
|
543
526
|
}));
|
|
544
527
|
// Broadcast task_updated so frontend re-fetches computed status
|
|
@@ -564,6 +547,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
564
547
|
defaultEnvironmentId: r.defaultEnvironmentId,
|
|
565
548
|
status: r.status,
|
|
566
549
|
useWorktrees: r.useWorktrees,
|
|
550
|
+
worktreeBasePath: r.worktreeBasePath,
|
|
567
551
|
createdAt: r.createdAt,
|
|
568
552
|
updatedAt: r.updatedAt,
|
|
569
553
|
})),
|
|
@@ -587,7 +571,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
587
571
|
}
|
|
588
572
|
// useWorktrees defaults to true when not specified
|
|
589
573
|
const createUseWorktrees = msg.payload?.useWorktrees ?? true;
|
|
590
|
-
projectStore.createProject(id, name, msg.payload?.description || "", msg.payload?.repoUrl || "", msg.payload?.defaultEnvironmentId || "", createUseWorktrees);
|
|
574
|
+
projectStore.createProject(id, name, msg.payload?.description || "", msg.payload?.repoUrl || "", msg.payload?.defaultEnvironmentId || "", createUseWorktrees, typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "");
|
|
591
575
|
const row = projectStore.getProject(id);
|
|
592
576
|
broadcast({ type: "project_created", payload: { project: row } });
|
|
593
577
|
break;
|
|
@@ -611,7 +595,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
611
595
|
return;
|
|
612
596
|
}
|
|
613
597
|
const nameVal = typeof msg.payload?.name === "string" ? msg.payload.name : undefined;
|
|
614
|
-
if (nameVal
|
|
598
|
+
if (nameVal?.trim() === "") {
|
|
615
599
|
sendWs(ws, { type: "error", payload: { message: "Project name cannot be empty" } });
|
|
616
600
|
return;
|
|
617
601
|
}
|
|
@@ -623,12 +607,14 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
623
607
|
return;
|
|
624
608
|
}
|
|
625
609
|
const worktreesVal = typeof msg.payload?.useWorktrees === "boolean" ? msg.payload.useWorktrees : undefined;
|
|
610
|
+
const worktreeBasePathVal = typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath : undefined;
|
|
626
611
|
projectStore.updateProject(projectId, {
|
|
627
612
|
name: nameVal !== undefined ? nameVal.trim() : undefined,
|
|
628
613
|
description: descVal,
|
|
629
614
|
repoUrl: repoVal,
|
|
630
615
|
defaultEnvironmentId: envVal,
|
|
631
616
|
useWorktrees: worktreesVal,
|
|
617
|
+
worktreeBasePath: worktreeBasePathVal,
|
|
632
618
|
});
|
|
633
619
|
broadcast({ type: "project_updated", payload: { projectId } });
|
|
634
620
|
break;
|
|
@@ -761,7 +747,6 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
761
747
|
branch: r.branch,
|
|
762
748
|
latestSessionId: computed.latestSessionId,
|
|
763
749
|
dependsOn: safeParseJsonArray(r.dependsOn),
|
|
764
|
-
reviewNotes: r.reviewNotes,
|
|
765
750
|
sortOrder: r.sortOrder,
|
|
766
751
|
createdAt: r.createdAt,
|
|
767
752
|
parentTaskId: r.parentTaskId,
|
|
@@ -827,7 +812,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
827
812
|
sendWs(ws, { type: "error", payload: { message: `Session not found: ${lateBindSessionId}` } });
|
|
828
813
|
return;
|
|
829
814
|
}
|
|
830
|
-
const terminalStatuses = [
|
|
815
|
+
const terminalStatuses = [SESSION_STATUS.COMPLETED, SESSION_STATUS.FAILED, SESSION_STATUS.INTERRUPTED];
|
|
831
816
|
if (terminalStatuses.includes(session.status)) {
|
|
832
817
|
sendWs(ws, {
|
|
833
818
|
type: "error",
|
|
@@ -857,8 +842,8 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
857
842
|
});
|
|
858
843
|
break;
|
|
859
844
|
}
|
|
860
|
-
// Only allow editing
|
|
861
|
-
if (
|
|
845
|
+
// Only allow editing not_started tasks (non-late-bind path)
|
|
846
|
+
if (existingTask.status !== TASK_STATUS.NOT_STARTED) {
|
|
862
847
|
sendWs(ws, {
|
|
863
848
|
type: "error",
|
|
864
849
|
payload: { message: `Task ${updateTaskId} cannot be edited (status: ${existingTask.status})` },
|
|
@@ -879,7 +864,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
879
864
|
.filter((d) => d !== updateTaskId)),
|
|
880
865
|
]
|
|
881
866
|
: safeParseJsonArray(existingTask.dependsOn);
|
|
882
|
-
taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn
|
|
867
|
+
taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn);
|
|
883
868
|
const updatedRow = taskStore.getTask(updateTaskId);
|
|
884
869
|
broadcast({
|
|
885
870
|
type: "task_updated",
|
|
@@ -908,7 +893,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
908
893
|
{
|
|
909
894
|
const taskSessions = sessionStore.listSessionsForTask(taskId);
|
|
910
895
|
const { status: effectiveStatus } = computeTaskStatus(task.status, taskSessions);
|
|
911
|
-
if (![
|
|
896
|
+
if (![TASK_STATUS.NOT_STARTED, TASK_STATUS.FAILED].includes(effectiveStatus)) {
|
|
912
897
|
sendWs(ws, {
|
|
913
898
|
type: "error",
|
|
914
899
|
payload: {
|
|
@@ -930,80 +915,88 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
930
915
|
model: msg.payload?.model,
|
|
931
916
|
personaId: msg.payload?.personaId || undefined,
|
|
932
917
|
environmentId: msg.payload?.environmentId || undefined,
|
|
918
|
+
notes: msg.payload?.notes || undefined,
|
|
933
919
|
});
|
|
934
920
|
if (startError) {
|
|
935
921
|
sendWs(ws, { type: "error", payload: { message: startError } });
|
|
936
922
|
}
|
|
937
923
|
break;
|
|
938
924
|
}
|
|
939
|
-
case "
|
|
925
|
+
case "complete_task": {
|
|
940
926
|
const taskId = msg.payload?.taskId;
|
|
941
927
|
if (!taskId)
|
|
942
928
|
return;
|
|
943
|
-
taskStore.
|
|
929
|
+
taskStore.markTaskComplete(taskId, TASK_STATUS.COMPLETE);
|
|
944
930
|
const task = taskStore.getTask(taskId);
|
|
945
931
|
const unblocked = task ? taskStore.checkAndUnblock(task.projectId) : [];
|
|
946
932
|
sendWs(ws, {
|
|
947
|
-
type: "
|
|
933
|
+
type: "task_completed",
|
|
948
934
|
payload: {
|
|
949
935
|
taskId,
|
|
950
936
|
unblockedTaskIds: unblocked.map((t) => t.id),
|
|
951
937
|
},
|
|
952
938
|
});
|
|
939
|
+
if (task) {
|
|
940
|
+
broadcast({
|
|
941
|
+
type: "task_completed",
|
|
942
|
+
payload: { taskId, projectId: task.projectId },
|
|
943
|
+
});
|
|
944
|
+
}
|
|
953
945
|
break;
|
|
954
946
|
}
|
|
955
|
-
case "
|
|
947
|
+
case "resume_task": {
|
|
956
948
|
const taskId = msg.payload?.taskId;
|
|
957
|
-
const reviewNotes = msg.payload?.reviewNotes || "";
|
|
958
949
|
if (!taskId)
|
|
959
950
|
return;
|
|
960
951
|
const task = taskStore.getTask(taskId);
|
|
961
|
-
if (!task)
|
|
952
|
+
if (!task) {
|
|
953
|
+
sendWs(ws, { type: "error", payload: { message: `Task not found: ${taskId}` } });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const latestSession = sessionStore.getLatestSessionForTask(taskId);
|
|
957
|
+
if (!latestSession) {
|
|
958
|
+
sendWs(ws, { type: "error", payload: { message: `Task ${taskId} has no sessions to resume` } });
|
|
962
959
|
return;
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const taskSessions = sessionStore.listSessionsByTaskIds([taskId]);
|
|
966
|
-
const { status: effectiveStatus } = computeTaskStatus(task.status, taskSessions);
|
|
967
|
-
if (effectiveStatus !== "review") {
|
|
960
|
+
}
|
|
961
|
+
if (![SESSION_STATUS.INTERRUPTED, SESSION_STATUS.COMPLETED].includes(latestSession.status)) {
|
|
968
962
|
sendWs(ws, {
|
|
969
963
|
type: "error",
|
|
970
|
-
payload: {
|
|
971
|
-
message: `Task cannot be rejected (status: ${effectiveStatus})`,
|
|
972
|
-
},
|
|
964
|
+
payload: { message: `Latest session ${latestSession.id} is not resumable (status: ${latestSession.status})` },
|
|
973
965
|
});
|
|
974
966
|
return;
|
|
975
967
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
// can derive the effective status from the new retry session. We use
|
|
981
|
-
// "pending" rather than "assigned" because rejection + retry is an
|
|
982
|
-
// automated flow — "assigned" implies deliberate human assignment.
|
|
983
|
-
taskStore.updateTask(task.id, task.title, task.description, "pending", safeParseJsonArray(task.dependsOn), reviewNotes);
|
|
984
|
-
broadcast({
|
|
985
|
-
type: "task_rejected",
|
|
986
|
-
payload: { taskId, projectId: task.projectId },
|
|
987
|
-
});
|
|
988
|
-
// Auto-retry: start a new session with the review feedback
|
|
989
|
-
const freshTask = taskStore.getTask(taskId);
|
|
990
|
-
if (freshTask) {
|
|
991
|
-
const retryError = await startTaskSession(ws, freshTask, {
|
|
992
|
-
runtime: previousSession?.runtime,
|
|
993
|
-
model: previousSession?.model,
|
|
994
|
-
environmentId: previousSession?.environmentId,
|
|
995
|
-
personaId: previousSession?.personaId,
|
|
968
|
+
if (!latestSession.runtimeSessionId) {
|
|
969
|
+
sendWs(ws, {
|
|
970
|
+
type: "error",
|
|
971
|
+
payload: { message: `Latest session ${latestSession.id} has no runtime session ID — cannot resume` },
|
|
996
972
|
});
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
payload: { taskId, projectId: freshTask.projectId },
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const env = envRegistry.getEnvironment(latestSession.environmentId);
|
|
976
|
+
if (!env) {
|
|
977
|
+
sendWs(ws, { type: "error", payload: { message: `Environment not found: ${latestSession.environmentId}` } });
|
|
978
|
+
return;
|
|
1006
979
|
}
|
|
980
|
+
const conn = await autoProvisionEnvironment(ws, latestSession.environmentId, env, { taskId });
|
|
981
|
+
if (!conn) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const powerlineReq = create(powerline.ResumeRequestSchema, {
|
|
985
|
+
sessionId: latestSession.id,
|
|
986
|
+
runtimeSessionId: latestSession.runtimeSessionId,
|
|
987
|
+
runtime: latestSession.runtime,
|
|
988
|
+
});
|
|
989
|
+
const logPath = latestSession.logPath || join(grackleHome, LOGS_DIR, latestSession.id);
|
|
990
|
+
processEventStream(conn.client.resume(powerlineReq), {
|
|
991
|
+
sessionId: latestSession.id,
|
|
992
|
+
logPath,
|
|
993
|
+
projectId: task.projectId,
|
|
994
|
+
taskId: task.id,
|
|
995
|
+
});
|
|
996
|
+
broadcast({
|
|
997
|
+
type: "task_started",
|
|
998
|
+
payload: { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId },
|
|
999
|
+
});
|
|
1007
1000
|
break;
|
|
1008
1001
|
}
|
|
1009
1002
|
case "delete_task": {
|
|
@@ -1037,12 +1030,12 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1037
1030
|
logger.warn({ taskId, sessionId: activeSession.id, err }, "Failed to kill session during task deletion");
|
|
1038
1031
|
}
|
|
1039
1032
|
}
|
|
1040
|
-
sessionStore.updateSession(activeSession.id,
|
|
1033
|
+
sessionStore.updateSession(activeSession.id, SESSION_STATUS.INTERRUPTED);
|
|
1041
1034
|
streamHub.publish(create(grackle.SessionEventSchema, {
|
|
1042
1035
|
sessionId: activeSession.id,
|
|
1043
1036
|
type: grackle.EventType.STATUS,
|
|
1044
1037
|
timestamp: new Date().toISOString(),
|
|
1045
|
-
content:
|
|
1038
|
+
content: SESSION_STATUS.INTERRUPTED,
|
|
1046
1039
|
raw: "",
|
|
1047
1040
|
}));
|
|
1048
1041
|
}
|
|
@@ -1126,7 +1119,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1126
1119
|
if (!taskId)
|
|
1127
1120
|
return;
|
|
1128
1121
|
const task = taskStore.getTask(taskId);
|
|
1129
|
-
if (!task
|
|
1122
|
+
if (!task?.branch) {
|
|
1130
1123
|
sendWs(ws, {
|
|
1131
1124
|
type: "task_diff",
|
|
1132
1125
|
payload: { taskId, error: "No branch" },
|
|
@@ -1402,7 +1395,10 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1402
1395
|
logger.warn({ err }, "Failed to list codespaces");
|
|
1403
1396
|
sendWs(ws, {
|
|
1404
1397
|
type: "codespaces_list",
|
|
1405
|
-
payload: {
|
|
1398
|
+
payload: {
|
|
1399
|
+
codespaces: [],
|
|
1400
|
+
error: formatGhError(err, "list codespaces"),
|
|
1401
|
+
},
|
|
1406
1402
|
});
|
|
1407
1403
|
}
|
|
1408
1404
|
break;
|
|
@@ -1417,15 +1413,17 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1417
1413
|
return;
|
|
1418
1414
|
}
|
|
1419
1415
|
const trimmedRepo = repo.trim();
|
|
1416
|
+
const machine = typeof msg.payload?.machine === "string"
|
|
1417
|
+
? msg.payload.machine.trim()
|
|
1418
|
+
: "";
|
|
1419
|
+
const createArgs = ["codespace", "create", "--repo", trimmedRepo];
|
|
1420
|
+
if (machine) {
|
|
1421
|
+
createArgs.push("--machine", machine);
|
|
1422
|
+
}
|
|
1420
1423
|
try {
|
|
1421
|
-
const result = await exec("gh",
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
"--repo",
|
|
1425
|
-
trimmedRepo,
|
|
1426
|
-
"--machine",
|
|
1427
|
-
"basicLinux32gb",
|
|
1428
|
-
], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
|
|
1424
|
+
const result = await exec("gh", createArgs, {
|
|
1425
|
+
timeout: GH_CODESPACE_CREATE_TIMEOUT_MS,
|
|
1426
|
+
});
|
|
1429
1427
|
const codespaceName = result.stdout.trim();
|
|
1430
1428
|
sendWs(ws, {
|
|
1431
1429
|
type: "codespace_created",
|
|
@@ -1436,7 +1434,7 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1436
1434
|
logger.error({ err, repo }, "Failed to create codespace");
|
|
1437
1435
|
sendWs(ws, {
|
|
1438
1436
|
type: "codespace_create_error",
|
|
1439
|
-
payload: { message:
|
|
1437
|
+
payload: { message: formatGhError(err, "create codespace") },
|
|
1440
1438
|
});
|
|
1441
1439
|
}
|
|
1442
1440
|
break;
|
|
@@ -1489,6 +1487,26 @@ async function handleMessage(ws, msg, subscriptions) {
|
|
|
1489
1487
|
broadcast({ type: "token_changed" });
|
|
1490
1488
|
break;
|
|
1491
1489
|
}
|
|
1490
|
+
case "get_credential_providers": {
|
|
1491
|
+
const config = credentialProviders.getCredentialProviders();
|
|
1492
|
+
sendWs(ws, {
|
|
1493
|
+
type: "credential_providers",
|
|
1494
|
+
payload: config,
|
|
1495
|
+
});
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
case "set_credential_providers": {
|
|
1499
|
+
if (!credentialProviders.isValidCredentialProviderConfig(msg.payload)) {
|
|
1500
|
+
sendWs(ws, { type: "error", payload: { message: "invalid credential provider config" } });
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
credentialProviders.setCredentialProviders(msg.payload);
|
|
1504
|
+
broadcast({
|
|
1505
|
+
type: "credential_providers",
|
|
1506
|
+
payload: credentialProviders.getCredentialProviders(),
|
|
1507
|
+
});
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1492
1510
|
}
|
|
1493
1511
|
}
|
|
1494
1512
|
function sendWs(ws, msg) {
|