@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.
Files changed (73) hide show
  1. package/dist/adapter-manager.d.ts.map +1 -1
  2. package/dist/adapter-manager.js +1 -0
  3. package/dist/adapter-manager.js.map +1 -1
  4. package/dist/adapters/codespace.js +1 -1
  5. package/dist/adapters/codespace.js.map +1 -1
  6. package/dist/adapters/docker.js +1 -1
  7. package/dist/adapters/docker.js.map +1 -1
  8. package/dist/adapters/local.js +1 -1
  9. package/dist/adapters/local.js.map +1 -1
  10. package/dist/adapters/remote-adapter-utils.d.ts.map +1 -1
  11. package/dist/adapters/remote-adapter-utils.js +22 -32
  12. package/dist/adapters/remote-adapter-utils.js.map +1 -1
  13. package/dist/adapters/ssh.js +1 -1
  14. package/dist/adapters/ssh.js.map +1 -1
  15. package/dist/compute-task-status.d.ts +11 -7
  16. package/dist/compute-task-status.d.ts.map +1 -1
  17. package/dist/compute-task-status.js +35 -40
  18. package/dist/compute-task-status.js.map +1 -1
  19. package/dist/credential-providers.d.ts +27 -0
  20. package/dist/credential-providers.d.ts.map +1 -0
  21. package/dist/credential-providers.js +166 -0
  22. package/dist/credential-providers.js.map +1 -0
  23. package/dist/db.d.ts.map +1 -1
  24. package/dist/db.js +55 -5
  25. package/dist/db.js.map +1 -1
  26. package/dist/event-processor.d.ts.map +1 -1
  27. package/dist/event-processor.js +15 -14
  28. package/dist/event-processor.js.map +1 -1
  29. package/dist/github-import.d.ts.map +1 -1
  30. package/dist/github-import.js +3 -2
  31. package/dist/github-import.js.map +1 -1
  32. package/dist/grpc-service.d.ts.map +1 -1
  33. package/dist/grpc-service.js +73 -41
  34. package/dist/grpc-service.js.map +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +23 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/project-store.d.ts +3 -1
  39. package/dist/project-store.d.ts.map +1 -1
  40. package/dist/project-store.js +5 -1
  41. package/dist/project-store.js.map +1 -1
  42. package/dist/schema.d.ts +66 -38
  43. package/dist/schema.d.ts.map +1 -1
  44. package/dist/schema.js +7 -3
  45. package/dist/schema.js.map +1 -1
  46. package/dist/session-store.d.ts.map +1 -1
  47. package/dist/session-store.js +4 -3
  48. package/dist/session-store.js.map +1 -1
  49. package/dist/task-store.d.ts +6 -6
  50. package/dist/task-store.d.ts.map +1 -1
  51. package/dist/task-store.js +11 -11
  52. package/dist/task-store.js.map +1 -1
  53. package/dist/token-broker.d.ts +6 -3
  54. package/dist/token-broker.d.ts.map +1 -1
  55. package/dist/token-broker.js +11 -26
  56. package/dist/token-broker.js.map +1 -1
  57. package/dist/transcript.js.map +1 -1
  58. package/dist/utils/format-gh-error.d.ts +6 -0
  59. package/dist/utils/format-gh-error.d.ts.map +1 -0
  60. package/dist/utils/format-gh-error.js +30 -0
  61. package/dist/utils/format-gh-error.js.map +1 -0
  62. package/dist/utils/system-context.d.ts +1 -1
  63. package/dist/utils/system-context.d.ts.map +1 -1
  64. package/dist/utils/system-context.js +2 -2
  65. package/dist/utils/system-context.js.map +1 -1
  66. package/dist/ws-bridge.d.ts.map +1 -1
  67. package/dist/ws-bridge.js +123 -105
  68. package/dist/ws-bridge.js.map +1 -1
  69. package/dist/ws-broadcast.d.ts +5 -0
  70. package/dist/ws-broadcast.d.ts.map +1 -1
  71. package/dist/ws-broadcast.js +20 -0
  72. package/dist/ws-broadcast.js.map +1 -1
  73. package/package.json +4 -4
@@ -0,0 +1,6 @@
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 declare function formatGhError(err: unknown, operation: string): string;
6
+ //# sourceMappingURL=format-gh-error.d.ts.map
@@ -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, reviewNotes: string, canDecompose?: boolean): 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,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CA+C9H"}
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, reviewNotes, canDecompose) {
2
+ export function buildTaskSystemContext(title, description, notes, canDecompose) {
3
3
  const sections = [
4
4
  `## Task: ${title}`,
5
5
  description,
6
- reviewNotes ? `## Review Feedback (from previous attempt)\n${reviewNotes}` : "",
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,WAAmB,EAAE,YAAsB;IACpH,MAAM,QAAQ,GAAa;QACzB,YAAY,KAAK,EAAE;QACnB,WAAW;QACX,WAAW,CAAC,CAAC,CAAC,+CAA+C,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE;QAC/E,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"}
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"}
@@ -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;AAmDtD,wGAAwG;AACxG,wBAAgB,cAAc,CAC5B,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GACvC,eAAe,CAuCjB"}
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
- persona?.runtime ||
218
- env.defaultRuntime ||
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, freshTask.reviewNotes, freshTask.canDecompose);
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 || !session.logPath) {
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 ? "/workspace" : "",
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 !== "waiting_input") {
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 waiting for input (status: ${session.status})`,
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 killed anyway");
516
+ logger.warn({ sessionId, err }, "PowerLine kill failed — marking session interrupted anyway");
534
517
  }
535
518
  }
536
- sessionStore.updateSession(sessionId, "killed");
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: "killed",
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 !== undefined && nameVal.trim() === "") {
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 = ["completed", "failed", "killed"];
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 pending/assigned tasks (non-late-bind path)
861
- if (!["pending", "assigned"].includes(existingTask.status)) {
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, existingTask.reviewNotes);
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 (!["pending", "assigned", "failed"].includes(effectiveStatus)) {
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 "approve_task": {
925
+ case "complete_task": {
940
926
  const taskId = msg.payload?.taskId;
941
927
  if (!taskId)
942
928
  return;
943
- taskStore.markTaskCompleted(taskId, "done");
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: "task_approved",
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 "reject_task": {
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
- // Use computed status (derived from session history) since the stored
964
- // status is never explicitly set to "review".
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
- // Preserve runtime/model from the previous session so the retry
977
- // doesn't unexpectedly switch runtimes/models.
978
- const previousSession = sessionStore.getLatestSessionForTask(task.id);
979
- // Store review notes and reset status to "pending" so computeTaskStatus
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
- if (retryError) {
998
- // Retry failed — set status to "assigned" so the user can retry manually.
999
- logger.warn({ taskId, error: retryError }, "Auto-retry after rejection failed — task set to assigned");
1000
- taskStore.updateTask(freshTask.id, freshTask.title, freshTask.description, "assigned", safeParseJsonArray(freshTask.dependsOn), freshTask.reviewNotes);
1001
- broadcast({
1002
- type: "task_updated",
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, "killed");
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: "killed",
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 || !task.branch) {
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: { codespaces: [], error: String(err) },
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
- "codespace",
1423
- "create",
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: String(err) },
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) {