@gajae-code/coding-agent 0.4.3 → 0.4.5

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 (92) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/types/async/job-manager.d.ts +19 -1
  3. package/dist/types/cli/fast-help.d.ts +1 -0
  4. package/dist/types/cli/setup-cli.d.ts +16 -1
  5. package/dist/types/commands/coordinator.d.ts +19 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/commands/mcp-serve.d.ts +24 -0
  8. package/dist/types/commands/setup.d.ts +47 -0
  9. package/dist/types/config/model-registry.d.ts +3 -0
  10. package/dist/types/config/models-config-schema.d.ts +5 -0
  11. package/dist/types/coordinator/contract.d.ts +4 -0
  12. package/dist/types/coordinator-mcp/policy.d.ts +24 -0
  13. package/dist/types/coordinator-mcp/safety.d.ts +26 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +58 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +13 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  17. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  18. package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
  19. package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
  20. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  21. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  22. package/dist/types/harness-control-plane/types.d.ts +9 -1
  23. package/dist/types/main.d.ts +2 -2
  24. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  25. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  26. package/dist/types/session/session-manager.d.ts +8 -0
  27. package/dist/types/setup/hermes-setup.d.ts +78 -0
  28. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  29. package/dist/types/task/receipt.d.ts +1 -0
  30. package/dist/types/task/render.d.ts +7 -1
  31. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  32. package/dist/types/task/types.d.ts +10 -0
  33. package/dist/types/tools/subagent-render.d.ts +25 -0
  34. package/dist/types/tools/subagent.d.ts +5 -1
  35. package/package.json +8 -7
  36. package/scripts/build-binary.ts +4 -0
  37. package/src/async/job-manager.ts +43 -1
  38. package/src/cli/fast-help.ts +80 -0
  39. package/src/cli/setup-cli.ts +95 -2
  40. package/src/cli.ts +109 -16
  41. package/src/commands/coordinator.ts +113 -0
  42. package/src/commands/harness.ts +92 -9
  43. package/src/commands/mcp-serve.ts +63 -0
  44. package/src/commands/setup.ts +34 -1
  45. package/src/config/models-config-schema.ts +1 -0
  46. package/src/coordinator/contract.ts +21 -0
  47. package/src/coordinator-mcp/policy.ts +160 -0
  48. package/src/coordinator-mcp/safety.ts +80 -0
  49. package/src/coordinator-mcp/server.ts +1519 -0
  50. package/src/cursor.ts +30 -2
  51. package/src/extensibility/extensions/types.ts +13 -0
  52. package/src/gjc-runtime/launch-worktree.ts +12 -1
  53. package/src/gjc-runtime/session-state-sidecar.ts +117 -0
  54. package/src/harness-control-plane/finalize.ts +39 -5
  55. package/src/harness-control-plane/owner.ts +9 -1
  56. package/src/harness-control-plane/phase-rollup.ts +96 -0
  57. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  58. package/src/harness-control-plane/receipts.ts +229 -1
  59. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  60. package/src/harness-control-plane/types.ts +29 -1
  61. package/src/internal-urls/docs-index.generated.ts +6 -4
  62. package/src/main.ts +7 -3
  63. package/src/modes/components/hook-selector.ts +109 -5
  64. package/src/modes/components/status-line.ts +6 -6
  65. package/src/modes/controllers/event-controller.ts +5 -4
  66. package/src/modes/controllers/extension-ui-controller.ts +16 -1
  67. package/src/modes/interactive-mode.ts +4 -5
  68. package/src/modes/print-mode.ts +1 -1
  69. package/src/modes/theme/theme.ts +2 -2
  70. package/src/modes/utils/abort-message.ts +41 -0
  71. package/src/modes/utils/context-usage.ts +15 -8
  72. package/src/modes/utils/ui-helpers.ts +5 -6
  73. package/src/prompts/agents/architect.md +6 -0
  74. package/src/prompts/agents/critic.md +6 -0
  75. package/src/prompts/agents/planner.md +8 -1
  76. package/src/sdk.ts +9 -4
  77. package/src/session/agent-session.ts +22 -5
  78. package/src/session/session-manager.ts +20 -0
  79. package/src/setup/hermes/templates/operator-instructions.v1.md +30 -0
  80. package/src/setup/hermes-setup.ts +484 -0
  81. package/src/task/fork-context-advisory.ts +99 -0
  82. package/src/task/index.ts +33 -2
  83. package/src/task/receipt.ts +2 -0
  84. package/src/task/render.ts +14 -0
  85. package/src/task/roi-reconciliation.ts +90 -0
  86. package/src/task/types.ts +7 -0
  87. package/src/tools/ask.ts +30 -10
  88. package/src/tools/index.ts +2 -2
  89. package/src/tools/renderers.ts +2 -0
  90. package/src/tools/subagent-render.ts +169 -0
  91. package/src/tools/subagent.ts +49 -7
  92. package/src/utils/title-generator.ts +16 -2
@@ -0,0 +1,1519 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as nodeFs from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { VERSION } from "@gajae-code/utils/dirs";
6
+ import {
7
+ COORDINATOR_MCP_PROTOCOL_VERSION,
8
+ COORDINATOR_MCP_SERVER_NAME,
9
+ COORDINATOR_MCP_TOOL_NAMES,
10
+ type CoordinatorToolName,
11
+ } from "../coordinator/contract";
12
+ import {
13
+ GJC_COORDINATOR_SESSION_ID_ENV,
14
+ GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
15
+ } from "../gjc-runtime/session-state-sidecar";
16
+ import {
17
+ assertCoordinatorArtifactPath,
18
+ assertCoordinatorWorkdir,
19
+ buildCoordinatorMcpConfig,
20
+ type CoordinatorMcpConfig,
21
+ coordinatorNamespacePath,
22
+ requireCoordinatorMutation,
23
+ } from "./policy";
24
+
25
+ export type { CoordinatorToolName };
26
+ export { COORDINATOR_MCP_PROTOCOL_VERSION, COORDINATOR_MCP_SERVER_NAME, COORDINATOR_MCP_TOOL_NAMES };
27
+
28
+ interface JsonRpcRequest {
29
+ jsonrpc: "2.0";
30
+ id?: string | number | null;
31
+ method: string;
32
+ params?: unknown;
33
+ }
34
+
35
+ type JsonRpcResult = any;
36
+
37
+ interface JsonRpcResponse {
38
+ jsonrpc: "2.0";
39
+ id: string | number | null;
40
+ result?: JsonRpcResult;
41
+ error?: { code: number; message: string; data?: unknown };
42
+ }
43
+
44
+ interface SessionStartInput {
45
+ cwd: string;
46
+ prompt?: string;
47
+ namespace: { profile: string | null; repo: string | null };
48
+ worktree: true;
49
+ }
50
+
51
+ interface SessionRegisterInput {
52
+ sessionId: string;
53
+ cwd: string;
54
+ tmuxSession: string;
55
+ tmuxTarget: string;
56
+ visible: boolean;
57
+ warpAttached: boolean | null;
58
+ source: string;
59
+ model: string | null;
60
+ }
61
+
62
+ interface CoordinatorFinalResponse {
63
+ text: string | null;
64
+ format: "markdown";
65
+ source: string | null;
66
+ artifact_path: string | null;
67
+ truncated: boolean;
68
+ }
69
+
70
+ function reportableFinalResponse(response: CoordinatorFinalResponse): boolean {
71
+ return (
72
+ (typeof response.text === "string" && response.text.trim().length > 0) ||
73
+ (typeof response.artifact_path === "string" && response.artifact_path.trim().length > 0)
74
+ );
75
+ }
76
+
77
+ interface RuntimeSessionStatePayload extends CoordinatorSessionState {
78
+ final_response?: CoordinatorFinalResponse;
79
+ error?: { code: string; message: string; recoverable: boolean } | null;
80
+ }
81
+
82
+ interface CoordinatorServices {
83
+ listSessions?: () => unknown[] | Promise<unknown[]>;
84
+ startSession?: (input: SessionStartInput) => unknown | Promise<unknown>;
85
+ commandRunner?: (command: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>;
86
+ }
87
+
88
+ interface CoordinatorMcpServerOptions {
89
+ env?: NodeJS.ProcessEnv;
90
+ services?: CoordinatorServices;
91
+ }
92
+
93
+ interface LegacyHandlerOptions {
94
+ env?: NodeJS.ProcessEnv;
95
+ createSession?: () => unknown;
96
+ }
97
+
98
+ type TurnStatus =
99
+ | "queued"
100
+ | "delivering"
101
+ | "active"
102
+ | "waiting_for_answer"
103
+ | "completing"
104
+ | "completed"
105
+ | "failed"
106
+ | "cancelled"
107
+ | "superseded";
108
+
109
+ interface TurnRecord {
110
+ schema_version: 1;
111
+ turn_id: string;
112
+ session_id: string;
113
+ namespace: { profile: string | null; repo: string | null };
114
+ status: TurnStatus;
115
+ prompt: { text: string; created_at: string; source: "mcp" | "question_answer" };
116
+ delivery: {
117
+ delivered: boolean;
118
+ queued: boolean;
119
+ target: string | null;
120
+ tmux_keys_sent?: boolean;
121
+ prompt_acknowledged?: boolean;
122
+ state?: "queued" | "tmux_keys_sent" | "acknowledged" | "unavailable";
123
+ attempts: Array<{
124
+ delivered: boolean;
125
+ created_at: string;
126
+ reason: string | null;
127
+ channel?: "tmux_keys" | "runtime_ack";
128
+ tmux_keys_sent?: boolean;
129
+ }>;
130
+ };
131
+ question_ids: string[];
132
+ final_response: {
133
+ text: string | null;
134
+ format: "markdown";
135
+ source: string | null;
136
+ artifact_path: string | null;
137
+ truncated: boolean;
138
+ };
139
+ evidence: Array<Record<string, unknown>>;
140
+ error: { code: string; message: string; recoverable: boolean } | null;
141
+ liveness: { checked_at: string | null; live: boolean | null; reason: string | null };
142
+ created_at: string;
143
+ updated_at: string;
144
+ started_at: string | null;
145
+ completed_at: string | null;
146
+ }
147
+
148
+ type CoordinatorSessionStateValue =
149
+ | "booting"
150
+ | "ready_for_input"
151
+ | "running"
152
+ | "needs_user_input"
153
+ | "completed"
154
+ | "errored"
155
+ | "stale"
156
+ | "unknown";
157
+
158
+ interface CoordinatorSessionState {
159
+ schema_version: 1;
160
+ session_id: string;
161
+ state: CoordinatorSessionStateValue;
162
+ ready_for_input: boolean;
163
+ current_turn_id: string | null;
164
+ last_turn_id: string | null;
165
+ updated_at: string;
166
+ source: "coordinator" | "agent_session_event";
167
+ live: boolean | null;
168
+ reason: string | null;
169
+ }
170
+
171
+ const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
172
+ const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
173
+ const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
174
+ const TURN_ID_PATTERN = /^turn-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
175
+ const SAFE_EXTERNAL_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}$/;
176
+ function asRecord(value: unknown): Record<string, unknown> | null {
177
+ return typeof value === "object" && value !== null && !Array.isArray(value)
178
+ ? (value as Record<string, unknown>)
179
+ : null;
180
+ }
181
+
182
+ function textResult(
183
+ payload: unknown,
184
+ isError = false,
185
+ ): { content: Array<{ type: "text"; text: string }>; isError: boolean } {
186
+ return {
187
+ content: [{ type: "text", text: typeof payload === "string" ? payload : JSON.stringify(payload) }],
188
+ isError,
189
+ };
190
+ }
191
+
192
+ function toolSchema(name: CoordinatorToolName): {
193
+ name: CoordinatorToolName;
194
+ description: string;
195
+ inputSchema: Record<string, unknown>;
196
+ } {
197
+ const allowMutation = { type: "boolean", description: "Required and must be true for mutating tools." };
198
+ const cwd = {
199
+ type: "string",
200
+ description: "Canonicalized GJC worktree or project directory inside configured roots.",
201
+ };
202
+ const sessionId = { type: "string", description: "GJC coordinator bridge session id." };
203
+ const pathField = { type: "string", description: "Artifact path inside configured safe roots." };
204
+ const common = { type: "object", properties: {} as Record<string, unknown> };
205
+ if (name === "gjc_coordinator_register_session") {
206
+ return {
207
+ name,
208
+ description: "Register an existing visible tmux GJC session as a coordinator-authoritative session.",
209
+ inputSchema: {
210
+ type: "object",
211
+ properties: {
212
+ session_id: sessionId,
213
+ cwd,
214
+ tmux_session: { type: "string" },
215
+ tmux_target: { type: "string" },
216
+ visible: { type: "boolean" },
217
+ warp_attached: { type: "boolean" },
218
+ source: { type: "string" },
219
+ model: { type: "string" },
220
+ allow_mutation: allowMutation,
221
+ },
222
+ required: ["session_id", "cwd", "tmux_session", "tmux_target", "allow_mutation"],
223
+ },
224
+ };
225
+ }
226
+ if (name === "gjc_coordinator_start_session") {
227
+ return {
228
+ name,
229
+ description: "Start a GJC worktree/tmux oriented session through the coordinator bridge.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: { cwd, prompt: { type: "string" }, allow_mutation: allowMutation },
233
+ required: ["cwd", "allow_mutation"],
234
+ },
235
+ };
236
+ }
237
+ if (name === "gjc_coordinator_send_prompt") {
238
+ return {
239
+ name,
240
+ description:
241
+ "Create a durable turn and deliver a bounded follow-up prompt for a selected coordinator bridge session.",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ session_id: sessionId,
246
+ prompt: { type: "string" },
247
+ queue: { type: "boolean" },
248
+ force: { type: "boolean" },
249
+ allow_mutation: allowMutation,
250
+ },
251
+ required: ["session_id", "prompt", "allow_mutation"],
252
+ },
253
+ };
254
+ }
255
+ if (name === "gjc_coordinator_read_turn") {
256
+ return {
257
+ name,
258
+ description: "Read authoritative durable turn state plus bounded advisory tmux status.",
259
+ inputSchema: {
260
+ type: "object",
261
+ properties: { session_id: sessionId, turn_id: { type: "string" }, lines: { type: "number" } },
262
+ required: ["turn_id"],
263
+ },
264
+ };
265
+ }
266
+ if (name === "gjc_coordinator_await_turn") {
267
+ return {
268
+ name,
269
+ description: "Poll a durable turn for a bounded time and return the same shape as read_turn.",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {
273
+ session_id: sessionId,
274
+ turn_id: { type: "string" },
275
+ timeout_ms: { type: "number" },
276
+ poll_interval_ms: { type: "number" },
277
+ lines: { type: "number" },
278
+ },
279
+ required: ["turn_id"],
280
+ },
281
+ };
282
+ }
283
+ if (name === "gjc_coordinator_submit_question_answer") {
284
+ return {
285
+ name,
286
+ description: "Submit a bounded structured answer by question id.",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ session_id: sessionId,
291
+ turn_id: { type: "string" },
292
+ question_id: { type: "string" },
293
+ answer: {},
294
+ allow_mutation: allowMutation,
295
+ },
296
+ required: ["question_id", "answer", "allow_mutation"],
297
+ },
298
+ };
299
+ }
300
+ if (name === "gjc_coordinator_report_status") {
301
+ return {
302
+ name,
303
+ description: "Write a bounded coordinator coordination status report.",
304
+ inputSchema: {
305
+ type: "object",
306
+ properties: {
307
+ session_id: sessionId,
308
+ turn_id: { type: "string" },
309
+ status: { type: "string" },
310
+ summary: { type: "string" },
311
+ blocker: { type: "string" },
312
+ pr_url: { type: "string" },
313
+ evidence_paths: { type: "array", items: { type: "string" } },
314
+ allow_mutation: allowMutation,
315
+ },
316
+ required: ["status", "allow_mutation"],
317
+ },
318
+ };
319
+ }
320
+ if (name === "gjc_coordinator_read_artifact") {
321
+ return {
322
+ name,
323
+ description: "Read one bounded artifact from configured safe roots.",
324
+ inputSchema: { type: "object", properties: { path: pathField }, required: ["path"] },
325
+ };
326
+ }
327
+ if (name === "gjc_coordinator_read_status") {
328
+ return {
329
+ name,
330
+ description: "Read selected coordinator bridge session status.",
331
+ inputSchema: { type: "object", properties: { session_id: sessionId } },
332
+ };
333
+ }
334
+ if (name === "gjc_coordinator_read_tail") {
335
+ return {
336
+ name,
337
+ description: "Read a bounded structured session tail, not tmux scrollback.",
338
+ inputSchema: { type: "object", properties: { session_id: sessionId, lines: { type: "number" } } },
339
+ };
340
+ }
341
+ if (name === "gjc_coordinator_list_questions") {
342
+ return {
343
+ name,
344
+ description: "List bounded structured questions for coordinator coordination.",
345
+ inputSchema: { type: "object", properties: { session_id: sessionId, status: { type: "string" } } },
346
+ };
347
+ }
348
+ if (name === "gjc_coordinator_list_artifacts") {
349
+ return { name, description: "List known safe artifact roots for coordinator coordination.", inputSchema: common };
350
+ }
351
+ if (name === "gjc_coordinator_read_coordination_status") {
352
+ return { name, description: "Read coordinator coordination reports.", inputSchema: common };
353
+ }
354
+ return { name, description: "List known scoped GJC coordinator bridge sessions.", inputSchema: common };
355
+ }
356
+
357
+ function normalizeSession(session: Record<string, unknown>): Record<string, unknown> {
358
+ return {
359
+ session_id: session.sessionId ?? session.session_id ?? session.name ?? "unknown",
360
+ ...(session.tmuxSession ? { tmux_session: session.tmuxSession } : {}),
361
+ ...(session.cwd ? { cwd: session.cwd } : {}),
362
+ ...(session.createdAt ? { created_at: session.createdAt } : {}),
363
+ ...session,
364
+ };
365
+ }
366
+
367
+ async function ensureDir(dir: string): Promise<void> {
368
+ await fs.mkdir(dir, { recursive: true });
369
+ }
370
+
371
+ async function readJsonFile(file: string): Promise<unknown | null> {
372
+ try {
373
+ return JSON.parse(await fs.readFile(file, "utf8"));
374
+ } catch {
375
+ return null;
376
+ }
377
+ }
378
+
379
+ async function writeJsonFile(file: string, value: unknown): Promise<void> {
380
+ await ensureDir(path.dirname(file));
381
+ await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
382
+ }
383
+
384
+ async function listJsonFiles(dir: string): Promise<unknown[]> {
385
+ try {
386
+ const entries = await fs.readdir(dir);
387
+ const values = await Promise.all(
388
+ entries.filter(entry => entry.endsWith(".json")).map(entry => readJsonFile(path.join(dir, entry))),
389
+ );
390
+ return values.filter(value => value !== null);
391
+ } catch {
392
+ return [];
393
+ }
394
+ }
395
+
396
+ function safeExternalId(kind: "session" | "question", value: unknown): string {
397
+ if (typeof value !== "string" || !SAFE_EXTERNAL_ID_PATTERN.test(value)) throw new Error(`invalid_${kind}_id`);
398
+ return value;
399
+ }
400
+
401
+ function safeTurnId(value: unknown): string {
402
+ if (typeof value !== "string" || !TURN_ID_PATTERN.test(value)) throw new Error("invalid_turn_id");
403
+ return value;
404
+ }
405
+
406
+ function safeTmuxSessionName(value: unknown): string {
407
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(value)) {
408
+ throw new Error("invalid_tmux_session");
409
+ }
410
+ return value;
411
+ }
412
+
413
+ function safeTmuxTarget(value: unknown): string {
414
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,160}$/.test(value)) {
415
+ throw new Error("invalid_tmux_target");
416
+ }
417
+ return value;
418
+ }
419
+
420
+ function optionalString(value: unknown): string | null {
421
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
422
+ }
423
+
424
+ function optionalBoolean(value: unknown): boolean | null {
425
+ return typeof value === "boolean" ? value : null;
426
+ }
427
+
428
+ function turnsDir(namespaceDir: string): string {
429
+ return path.join(namespaceDir, "turns");
430
+ }
431
+
432
+ function activeTurnFile(namespaceDir: string, sessionId: string): string {
433
+ return path.join(namespaceDir, "active-turns", `${safeExternalId("session", sessionId)}.json`);
434
+ }
435
+
436
+ function turnFile(namespaceDir: string, turnId: string): string {
437
+ return path.join(turnsDir(namespaceDir), `${safeTurnId(turnId)}.json`);
438
+ }
439
+
440
+ function questionFile(namespaceDir: string, questionId: string): string {
441
+ return path.join(namespaceDir, "questions", `${safeExternalId("question", questionId)}.json`);
442
+ }
443
+
444
+ function sessionStateFile(namespaceDir: string, sessionId: string): string {
445
+ return path.join(namespaceDir, "session-states", `${safeExternalId("session", sessionId)}.json`);
446
+ }
447
+
448
+ async function readTurnRecord(namespaceDir: string, turnId: unknown): Promise<TurnRecord | null> {
449
+ return (await readJsonFile(turnFile(namespaceDir, safeTurnId(turnId)))) as TurnRecord | null;
450
+ }
451
+
452
+ async function writeTurnRecord(namespaceDir: string, turn: TurnRecord): Promise<void> {
453
+ await writeJsonFile(turnFile(namespaceDir, turn.turn_id), turn);
454
+ }
455
+
456
+ async function readActiveTurn(namespaceDir: string, sessionId: string): Promise<TurnRecord | null> {
457
+ const active = asRecord(await readJsonFile(activeTurnFile(namespaceDir, sessionId)));
458
+ if (!active || typeof active.turn_id !== "string") return null;
459
+ const turn = await readTurnRecord(namespaceDir, active.turn_id);
460
+ if (!turn || turn.session_id !== sessionId || !ACTIVE_TURN_STATUSES.has(turn.status)) return null;
461
+ return turn;
462
+ }
463
+
464
+ async function writeActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
465
+ await writeJsonFile(activeTurnFile(namespaceDir, turn.session_id), {
466
+ session_id: turn.session_id,
467
+ turn_id: turn.turn_id,
468
+ status: turn.status,
469
+ updated_at: turn.updated_at,
470
+ });
471
+ }
472
+
473
+ async function clearActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
474
+ const active = asRecord(await readJsonFile(activeTurnFile(namespaceDir, turn.session_id)));
475
+ if (active?.turn_id === turn.turn_id) await fs.rm(activeTurnFile(namespaceDir, turn.session_id), { force: true });
476
+ }
477
+
478
+ async function readSessionState(namespaceDir: string, sessionId: string): Promise<CoordinatorSessionState | null> {
479
+ return (await readJsonFile(sessionStateFile(namespaceDir, sessionId))) as CoordinatorSessionState | null;
480
+ }
481
+
482
+ async function writeSessionState(
483
+ namespaceDir: string,
484
+ sessionId: string,
485
+ state: CoordinatorSessionStateValue,
486
+ options: {
487
+ currentTurnId?: string | null;
488
+ lastTurnId?: string | null;
489
+ live?: boolean | null;
490
+ reason?: string | null;
491
+ source?: CoordinatorSessionState["source"];
492
+ } = {},
493
+ ): Promise<CoordinatorSessionState> {
494
+ const previous = await readSessionState(namespaceDir, sessionId);
495
+ const payload: CoordinatorSessionState = {
496
+ schema_version: 1,
497
+ session_id: sessionId,
498
+ state,
499
+ ready_for_input: state === "ready_for_input" || state === "completed",
500
+ current_turn_id: options.currentTurnId ?? (state === "running" ? (previous?.current_turn_id ?? null) : null),
501
+ last_turn_id: options.lastTurnId ?? previous?.last_turn_id ?? null,
502
+ updated_at: new Date().toISOString(),
503
+ source: options.source ?? "coordinator",
504
+ live: options.live ?? previous?.live ?? null,
505
+ reason: options.reason ?? null,
506
+ };
507
+ await writeJsonFile(sessionStateFile(namespaceDir, sessionId), payload);
508
+ return payload;
509
+ }
510
+
511
+ function hasTmuxIdentity(session: Record<string, unknown>): boolean {
512
+ return (
513
+ (typeof session.tmux_session === "string" && session.tmux_session.length > 0) ||
514
+ (typeof session.tmuxSession === "string" && session.tmuxSession.length > 0)
515
+ );
516
+ }
517
+
518
+ async function markTurnFailedForUnavailableSession(
519
+ namespaceDir: string,
520
+ turn: TurnRecord,
521
+ reason: string,
522
+ ): Promise<TurnRecord> {
523
+ const timestamp = new Date().toISOString();
524
+ const failed: TurnRecord = {
525
+ ...turn,
526
+ status: "failed",
527
+ final_response: {
528
+ text: `Coordinator session unavailable: ${reason}`,
529
+ format: "markdown",
530
+ source: "coordinator_liveness",
531
+ artifact_path: null,
532
+ truncated: false,
533
+ },
534
+ error: { code: "session_unavailable", message: reason, recoverable: true },
535
+ liveness: { checked_at: timestamp, live: false, reason },
536
+ updated_at: timestamp,
537
+ completed_at: timestamp,
538
+ };
539
+ await writeTurnRecord(namespaceDir, failed);
540
+ await clearActiveTurn(namespaceDir, failed);
541
+ await writeSessionState(namespaceDir, failed.session_id, "stale", {
542
+ lastTurnId: failed.turn_id,
543
+ live: false,
544
+ reason,
545
+ });
546
+ return failed;
547
+ }
548
+
549
+ async function markTurnTerminalFromSessionState(
550
+ namespaceDir: string,
551
+ turn: TurnRecord,
552
+ sessionState: CoordinatorSessionState,
553
+ ): Promise<TurnRecord> {
554
+ const terminalStatus: TurnStatus = sessionState.state === "errored" ? "failed" : "completed";
555
+ const runtimeState = sessionState as RuntimeSessionStatePayload;
556
+ const finalResponse = runtimeState.final_response ?? {
557
+ text: null,
558
+ format: "markdown" as const,
559
+ source: "runtime_state",
560
+ artifact_path: null,
561
+ truncated: false,
562
+ };
563
+ const timestamp = new Date().toISOString();
564
+ const resolved: TurnRecord = {
565
+ ...turn,
566
+ status: terminalStatus,
567
+ delivery: {
568
+ ...turn.delivery,
569
+ prompt_acknowledged: true,
570
+ state: "acknowledged",
571
+ },
572
+ final_response: finalResponse,
573
+ evidence: reportableFinalResponse(finalResponse)
574
+ ? turn.evidence
575
+ : [
576
+ ...turn.evidence,
577
+ {
578
+ type: MISSING_FINAL_RESPONSE_ADVISORY,
579
+ message: "Runtime completed without reportable final_response text or artifact_path.",
580
+ created_at: timestamp,
581
+ },
582
+ ],
583
+ error:
584
+ terminalStatus === "failed"
585
+ ? (runtimeState.error ?? {
586
+ code: "runtime_errored",
587
+ message: sessionState.reason ?? "runtime_errored",
588
+ recoverable: true,
589
+ })
590
+ : null,
591
+ updated_at: timestamp,
592
+ completed_at: timestamp,
593
+ };
594
+ await writeTurnRecord(namespaceDir, resolved);
595
+ await clearActiveTurn(namespaceDir, resolved);
596
+ await writeSessionState(namespaceDir, resolved.session_id, sessionState.state, {
597
+ lastTurnId: resolved.turn_id,
598
+ live: sessionState.live,
599
+ reason: sessionState.reason,
600
+ });
601
+ return resolved;
602
+ }
603
+
604
+ function shellQuote(value: string): string {
605
+ return `'${value.replaceAll("'", "'\\''")}'`;
606
+ }
607
+ function makeTurnRecord(
608
+ config: CoordinatorMcpConfig,
609
+ sessionId: string,
610
+ prompt: string,
611
+ status: TurnStatus,
612
+ ): TurnRecord {
613
+ const timestamp = new Date().toISOString();
614
+ return {
615
+ schema_version: 1,
616
+ turn_id: `turn-${randomUUID()}`,
617
+ session_id: sessionId,
618
+ namespace: config.namespace,
619
+ status,
620
+ prompt: { text: prompt, created_at: timestamp, source: "mcp" },
621
+ delivery: {
622
+ delivered: false,
623
+ queued: true,
624
+ target: null,
625
+ tmux_keys_sent: false,
626
+ prompt_acknowledged: false,
627
+ state: "queued",
628
+ attempts: [],
629
+ },
630
+ question_ids: [],
631
+ final_response: { text: null, format: "markdown", source: null, artifact_path: null, truncated: false },
632
+ evidence: [],
633
+ error: null,
634
+ liveness: { checked_at: null, live: null, reason: null },
635
+ created_at: timestamp,
636
+ updated_at: timestamp,
637
+ started_at: status === "queued" ? null : timestamp,
638
+ completed_at: null,
639
+ };
640
+ }
641
+
642
+ function asTerminalTurnStatus(status: unknown): TurnStatus | null {
643
+ const normalized = String(status ?? "")
644
+ .trim()
645
+ .toLowerCase();
646
+ if (TERMINAL_TURN_STATUSES.has(normalized as TurnStatus)) return normalized as TurnStatus;
647
+ if (normalized === "blocked") return "failed";
648
+ return null;
649
+ }
650
+
651
+ function boundedTimeoutMs(value: unknown): number {
652
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
653
+ if (!Number.isFinite(parsed) || parsed <= 0) return 1000;
654
+ return Math.min(parsed, 30_000);
655
+ }
656
+
657
+ function boundedPollIntervalMs(value: unknown): number {
658
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
659
+ if (!Number.isFinite(parsed) || parsed <= 0) return 100;
660
+ return Math.min(Math.max(parsed, 10), 1000);
661
+ }
662
+ async function runCommand(command: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
663
+ const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
664
+ const [stdout, stderr, exitCode] = await Promise.all([
665
+ new Response(proc.stdout).text(),
666
+ new Response(proc.stderr).text(),
667
+ proc.exited,
668
+ ]);
669
+ return { exitCode, stdout, stderr };
670
+ }
671
+
672
+ type CommandRunner = (command: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>;
673
+
674
+ async function sendTmuxPromptKeys(
675
+ target: string,
676
+ prompt: string,
677
+ runner: CommandRunner = runCommand,
678
+ ): Promise<boolean> {
679
+ const sent = await runner(["tmux", "send-keys", "-t", target, prompt, "C-m", "C-m"]);
680
+ return sent.exitCode === 0;
681
+ }
682
+
683
+ function boundedLineCount(value: unknown): number {
684
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
685
+ if (!Number.isFinite(parsed) || parsed <= 0) return 80;
686
+ return Math.min(parsed, 400);
687
+ }
688
+
689
+ async function assertTmuxTargetAvailable(
690
+ tmuxSession: string,
691
+ tmuxTarget: string,
692
+ runner: CommandRunner = runCommand,
693
+ ): Promise<void> {
694
+ const session = await runner(["tmux", "has-session", "-t", tmuxSession]);
695
+ if (session.exitCode !== 0) throw new Error("tmux_session_unavailable");
696
+ const pane = await runner(["tmux", "display-message", "-p", "-t", tmuxTarget, "#{pane_id}"]);
697
+ if (pane.exitCode !== 0 || pane.stdout.trim().length === 0) throw new Error("tmux_target_unavailable");
698
+ }
699
+
700
+ async function registerExistingTmuxSession(
701
+ input: SessionRegisterInput,
702
+ namespaceDir: string,
703
+ sessionFilePath: string,
704
+ runner: CommandRunner = runCommand,
705
+ ): Promise<{ session: Record<string, unknown>; sessionState: CoordinatorSessionState }> {
706
+ await assertTmuxTargetAvailable(input.tmuxSession, input.tmuxTarget, runner);
707
+ const existing = asRecord(await readJsonFile(sessionFilePath));
708
+ if (existing) {
709
+ const existingSession = typeof existing.tmux_session === "string" ? existing.tmux_session : existing.tmuxSession;
710
+ const existingTarget = typeof existing.tmux_target === "string" ? existing.tmux_target : existing.tmuxTarget;
711
+ if (existingSession && existingSession !== input.tmuxSession) throw new Error("session_id_conflict");
712
+ if (existingTarget && existingTarget !== input.tmuxTarget) throw new Error("session_id_conflict");
713
+ }
714
+ const timestamp = new Date().toISOString();
715
+ const session = {
716
+ ...(existing ?? {}),
717
+ session_id: input.sessionId,
718
+ sessionId: input.sessionId,
719
+ tmux_session: input.tmuxSession,
720
+ tmuxSession: input.tmuxSession,
721
+ tmux_target: input.tmuxTarget,
722
+ tmuxTarget: input.tmuxTarget,
723
+ cwd: input.cwd,
724
+ created_at: typeof existing?.created_at === "string" ? existing.created_at : timestamp,
725
+ createdAt: typeof existing?.createdAt === "string" ? existing.createdAt : timestamp,
726
+ registered_at: timestamp,
727
+ visible: input.visible,
728
+ authoritative: true,
729
+ warp_attached: input.warpAttached,
730
+ source: input.source,
731
+ model: input.model,
732
+ };
733
+ await writeJsonFile(sessionFilePath, session);
734
+ const state = await writeSessionState(namespaceDir, input.sessionId, "ready_for_input", {
735
+ live: true,
736
+ reason: null,
737
+ });
738
+ return { session, sessionState: state };
739
+ }
740
+
741
+ async function startTmuxSession(
742
+ config: CoordinatorMcpConfig,
743
+ input: SessionStartInput,
744
+ namespaceDir: string,
745
+ ): Promise<Record<string, unknown>> {
746
+ if (!config.sessionCommand) throw new Error("coordinator_session_command_required");
747
+ const sessionName = `gjc-coordinator-${randomUUID().slice(0, 8)}`;
748
+ const runtimeStateFile = sessionStateFile(namespaceDir, sessionName);
749
+ const sessionCommand = [
750
+ "exec env",
751
+ `${GJC_COORDINATOR_SESSION_STATE_FILE_ENV}=${shellQuote(runtimeStateFile)}`,
752
+ `${GJC_COORDINATOR_SESSION_ID_ENV}=${shellQuote(sessionName)}`,
753
+ config.sessionCommand,
754
+ ].join(" ");
755
+ const started = await runCommand([
756
+ "tmux",
757
+ "new-session",
758
+ "-d",
759
+ "-P",
760
+ "-F",
761
+ "#{session_name}:#{window_index}.#{pane_index} #{pane_id}",
762
+ "-s",
763
+ sessionName,
764
+ "-c",
765
+ input.cwd,
766
+ sessionCommand,
767
+ ]);
768
+ if (started.exitCode !== 0) throw new Error(`coordinator_tmux_start_failed:${started.stderr || started.stdout}`);
769
+ const [tmuxTarget, paneId] = started.stdout.trim().split(/\s+/, 2);
770
+ const initialPromptTmuxKeysSent = input.prompt
771
+ ? await sendTmuxPromptKeys(tmuxTarget || sessionName, input.prompt)
772
+ : false;
773
+ return {
774
+ sessionId: sessionName,
775
+ tmuxSession: sessionName,
776
+ tmuxTarget: tmuxTarget || sessionName,
777
+ paneId,
778
+ cwd: input.cwd,
779
+ createdAt: new Date().toISOString(),
780
+ sessionCommand: config.sessionCommand,
781
+ runtimeStateFile,
782
+ initialPromptTmuxKeysSent,
783
+ };
784
+ }
785
+
786
+ async function captureTmuxTail(session: Record<string, unknown>, lines: number): Promise<string[]> {
787
+ const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
788
+ if (typeof target !== "string" || target.length === 0) return [];
789
+ const captured = await runCommand(["tmux", "capture-pane", "-t", target, "-p", "-S", `-${lines}`]);
790
+ if (captured.exitCode !== 0) return [];
791
+ return captured.stdout.split("\n").slice(-lines);
792
+ }
793
+
794
+ async function sendTmuxPrompt(
795
+ session: Record<string, unknown>,
796
+ prompt: string,
797
+ runner: CommandRunner = runCommand,
798
+ ): Promise<boolean> {
799
+ const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
800
+ if (typeof target !== "string" || target.length === 0) return false;
801
+ return await sendTmuxPromptKeys(target, prompt, runner);
802
+ }
803
+
804
+ async function hasTmuxSession(
805
+ session: Record<string, unknown>,
806
+ runner: CommandRunner = runCommand,
807
+ ): Promise<boolean | null> {
808
+ const tmuxSession = typeof session.tmux_session === "string" ? session.tmux_session : session.tmuxSession;
809
+ if (typeof tmuxSession !== "string" || tmuxSession.length === 0) return null;
810
+ const checked = await runner(["tmux", "has-session", "-t", tmuxSession]);
811
+ return checked.exitCode === 0;
812
+ }
813
+
814
+ function lastMatchingLine(lines: string[], pattern: RegExp): string | null {
815
+ for (let index = lines.length - 1; index >= 0; index--) {
816
+ const line = lines[index]?.trim();
817
+ if (line && pattern.test(line)) return line;
818
+ }
819
+ return null;
820
+ }
821
+
822
+ function summarizePaneTail(lines: string[]): Record<string, unknown> {
823
+ const nonEmpty = lines.map(line => line.trim()).filter(Boolean);
824
+ const spinnerLine = lastMatchingLine(nonEmpty, /^[⠁-⣿]\s+/u);
825
+ const hudLine = lastMatchingLine(nonEmpty, /\/ 📁 | PR \d+|Status Review|Tracking/i);
826
+ const errorLine = lastMatchingLine(nonEmpty, /\b(error|failed|exception|404|not_found)\b/i);
827
+ const assistantLine = lastMatchingLine(nonEmpty, /^(gajae|assistant)\b/i);
828
+ const lastContent = nonEmpty.at(-1) ?? null;
829
+ return {
830
+ state: spinnerLine ? "working" : errorLine ? "error_or_warning" : "idle_or_unknown",
831
+ activity: spinnerLine ?? hudLine ?? lastContent,
832
+ hud: hudLine,
833
+ last_error: errorLine,
834
+ last_speaker: assistantLine,
835
+ last_content: lastContent,
836
+ };
837
+ }
838
+
839
+ async function inspectTmuxSession(
840
+ session: Record<string, unknown>,
841
+ lines = 80,
842
+ runner: CommandRunner = runCommand,
843
+ ): Promise<Record<string, unknown>> {
844
+ const live = await hasTmuxSession(session, runner);
845
+ const tail = live ? await captureTmuxTail(session, lines) : [];
846
+ return {
847
+ live,
848
+ ...summarizePaneTail(tail),
849
+ tail_preview: tail.slice(-20),
850
+ };
851
+ }
852
+
853
+ function waitForTurnStateChange(namespaceDir: string, turn: TurnRecord, timeoutMs: number): Promise<void> {
854
+ const deferred = Promise.withResolvers<void>();
855
+ const watchers: nodeFs.FSWatcher[] = [];
856
+ const watchedFiles = new Map<string, Set<string>>([
857
+ [turnsDir(namespaceDir), new Set([`${turn.turn_id}.json`])],
858
+ [path.join(namespaceDir, "active-turns"), new Set([`${turn.session_id}.json`])],
859
+ [path.join(namespaceDir, "session-states"), new Set([`${turn.session_id}.json`])],
860
+ ]);
861
+ let settled = false;
862
+ const finish = () => {
863
+ if (settled) return;
864
+ settled = true;
865
+ for (const watcher of watchers) watcher.close();
866
+ clearTimeout(timer);
867
+ deferred.resolve();
868
+ };
869
+ const timer = setTimeout(finish, Math.max(timeoutMs, 0));
870
+ timer.unref?.();
871
+
872
+ for (const [dir, filenames] of watchedFiles) {
873
+ try {
874
+ const watcher = nodeFs.watch(dir, (_eventType, filename) => {
875
+ if (typeof filename === "string" && filenames.has(filename)) finish();
876
+ });
877
+ watchers.push(watcher);
878
+ } catch {
879
+ // Directory may not exist yet; the timeout remains a bounded fallback.
880
+ }
881
+ }
882
+
883
+ return deferred.promise;
884
+ }
885
+
886
+ function decodeUtf8WithinByteCap(bytes: Buffer, byteCap: number): string {
887
+ const decoder = new TextDecoder("utf-8", { fatal: true });
888
+ for (let end = Math.min(bytes.length, byteCap); end >= 0; end--) {
889
+ try {
890
+ const text = decoder.decode(bytes.subarray(0, end));
891
+ if (Buffer.byteLength(text) <= byteCap) return text;
892
+ } catch {
893
+ // Keep trimming until the byte slice ends on a valid UTF-8 boundary.
894
+ }
895
+ }
896
+ return "";
897
+ }
898
+
899
+ export async function readCoordinatorArtifact(
900
+ config: CoordinatorMcpConfig,
901
+ args: { path: unknown },
902
+ ): Promise<Record<string, unknown>> {
903
+ let handle: fs.FileHandle | null = null;
904
+ try {
905
+ const resolved = await assertCoordinatorArtifactPath(config, args.path);
906
+ handle = await fs.open(resolved.path, "r");
907
+ const readLimit = resolved.byteCap + 1;
908
+ const buffer = Buffer.alloc(readLimit);
909
+ const { bytesRead } = await handle.read(buffer, 0, readLimit, 0);
910
+ const boundedBytes = buffer.subarray(0, Math.min(bytesRead, resolved.byteCap));
911
+ const text = decodeUtf8WithinByteCap(boundedBytes, resolved.byteCap);
912
+ return {
913
+ ok: true,
914
+ path: resolved.path,
915
+ text,
916
+ bytes: Buffer.byteLength(text),
917
+ truncated: bytesRead > resolved.byteCap,
918
+ };
919
+ } catch (error) {
920
+ return {
921
+ ok: false,
922
+ reason: (error instanceof Error ? error.message.split(":")[0] : String(error)).replace(/^coordinator_/, ""),
923
+ };
924
+ } finally {
925
+ await handle?.close();
926
+ }
927
+ }
928
+
929
+ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions = {}) {
930
+ const config = buildCoordinatorMcpConfig(options.env ?? process.env);
931
+ const services = options.services ?? {};
932
+ const namespaceDir = coordinatorNamespacePath(config);
933
+ const commandRunner = services.commandRunner ?? runCommand;
934
+
935
+ async function listSessions(): Promise<unknown[]> {
936
+ if (!config.namespace.profile || !config.namespace.repo) return [];
937
+ if (services.listSessions) return await services.listSessions();
938
+ return await listJsonFiles(path.join(namespaceDir, "sessions"));
939
+ }
940
+ function sessionFile(sessionId: unknown): string {
941
+ return path.join(namespaceDir, "sessions", `${safeExternalId("session", sessionId)}.json`);
942
+ }
943
+ async function listQuestions(args: Record<string, unknown>): Promise<unknown[]> {
944
+ const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
945
+ const status = typeof args.status === "string" && args.status.length > 0 ? args.status : null;
946
+ return (await listJsonFiles(path.join(namespaceDir, "questions"))).filter(question => {
947
+ const record = asRecord(question);
948
+ if (!record) return false;
949
+ if (sessionId && record.session_id !== sessionId) return false;
950
+ if (status && record.status !== status) return false;
951
+ return true;
952
+ });
953
+ }
954
+
955
+ async function validateEvidencePaths(value: unknown): Promise<Array<{ path: string }>> {
956
+ if (value == null) return [];
957
+ if (!Array.isArray(value)) throw new Error("coordinator_evidence_paths_must_be_array");
958
+ const evidence: Array<{ path: string }> = [];
959
+ for (const item of value) {
960
+ const resolved = await assertCoordinatorArtifactPath(config, item);
961
+ evidence.push({ path: resolved.path });
962
+ }
963
+ return evidence;
964
+ }
965
+
966
+ async function activateTurn(session: Record<string, unknown>, turn: TurnRecord): Promise<TurnRecord> {
967
+ const tmuxKeysSent = await sendTmuxPrompt(session, turn.prompt.text, commandRunner);
968
+ const timestamp = new Date().toISOString();
969
+ const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
970
+ const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
971
+ const activeTurn: TurnRecord = {
972
+ ...turn,
973
+ status: "active",
974
+ delivery: {
975
+ delivered: false,
976
+ queued: !tmuxKeysSent,
977
+ target: typeof target === "string" ? target : null,
978
+ tmux_keys_sent: tmuxKeysSent,
979
+ prompt_acknowledged: false,
980
+ state: tmuxKeysSent ? "tmux_keys_sent" : "unavailable",
981
+ attempts: [
982
+ {
983
+ delivered: false,
984
+ tmux_keys_sent: tmuxKeysSent,
985
+ channel: "tmux_keys",
986
+ created_at: timestamp,
987
+ reason: tmuxKeysSent ? "awaiting_runtime_ack" : "tmux_delivery_unavailable",
988
+ },
989
+ ],
990
+ },
991
+ liveness: { checked_at: timestamp, live, reason: live === false ? "tmux_session_missing" : null },
992
+ started_at: turn.started_at ?? timestamp,
993
+ updated_at: timestamp,
994
+ };
995
+ await writeActiveTurn(namespaceDir, activeTurn);
996
+ await writeSessionState(namespaceDir, activeTurn.session_id, tmuxKeysSent ? "running" : "stale", {
997
+ currentTurnId: activeTurn.turn_id,
998
+ live,
999
+ reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
1000
+ });
1001
+ await writeTurnRecord(namespaceDir, activeTurn);
1002
+ return activeTurn;
1003
+ }
1004
+
1005
+ async function promoteNextQueuedTurn(sessionId: string): Promise<TurnRecord | null> {
1006
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
1007
+ if (!session) return null;
1008
+ const queuedTurns = (await listJsonFiles(turnsDir(namespaceDir)))
1009
+ .map(turn => asRecord(turn) as TurnRecord | null)
1010
+ .filter((turn): turn is TurnRecord => turn?.session_id === sessionId && turn.status === "queued")
1011
+ .sort((left, right) => left.created_at.localeCompare(right.created_at));
1012
+ const nextTurn = queuedTurns[0];
1013
+ return nextTurn ? await activateTurn(session, nextTurn) : null;
1014
+ }
1015
+
1016
+ async function readTurnPayload(
1017
+ turnId: unknown,
1018
+ sessionId: unknown,
1019
+ lines: unknown,
1020
+ ): Promise<Record<string, unknown>> {
1021
+ const turn = await readTurnRecord(namespaceDir, turnId);
1022
+ if (!turn) return { ok: false, reason: "unknown_turn" };
1023
+ if (sessionId != null && turn.session_id !== safeExternalId("session", sessionId)) {
1024
+ return { ok: false, reason: "turn_session_mismatch" };
1025
+ }
1026
+ const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
1027
+ let resolvedTurn = turn;
1028
+ let advisoryStatus: Record<string, unknown> = { live: false };
1029
+ let sessionState = await readSessionState(namespaceDir, turn.session_id);
1030
+ if (
1031
+ sessionState &&
1032
+ ACTIVE_TURN_STATUSES.has(turn.status) &&
1033
+ sessionState.current_turn_id === turn.turn_id &&
1034
+ (sessionState.state === "completed" || sessionState.state === "errored")
1035
+ ) {
1036
+ resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir, turn, sessionState);
1037
+ sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1038
+ } else if (
1039
+ sessionState &&
1040
+ ACTIVE_TURN_STATUSES.has(turn.status) &&
1041
+ sessionState.current_turn_id === turn.turn_id &&
1042
+ sessionState.state === "stale" &&
1043
+ sessionState.reason === "tmux_delivery_unavailable" &&
1044
+ turn.delivery.state === "unavailable" &&
1045
+ session &&
1046
+ hasTmuxIdentity(session)
1047
+ ) {
1048
+ resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "tmux_delivery_unavailable");
1049
+ sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1050
+ } else if (!session && ACTIVE_TURN_STATUSES.has(turn.status)) {
1051
+ resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "session_record_missing");
1052
+ sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1053
+ } else if (session) {
1054
+ advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines), commandRunner);
1055
+ if (ACTIVE_TURN_STATUSES.has(turn.status) && hasTmuxIdentity(session) && advisoryStatus.live === false) {
1056
+ resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "tmux_session_missing");
1057
+ sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
1058
+ }
1059
+ }
1060
+ const missingFinalResponse =
1061
+ resolvedTurn.status === "completed" && !reportableFinalResponse(resolvedTurn.final_response);
1062
+ return {
1063
+ ok: true,
1064
+ turn: resolvedTurn,
1065
+ advisory_status: advisoryStatus,
1066
+ session_state: sessionState,
1067
+ ...(missingFinalResponse
1068
+ ? {
1069
+ completion_missing_final_response: true,
1070
+ advisory: MISSING_FINAL_RESPONSE_ADVISORY,
1071
+ }
1072
+ : {}),
1073
+ };
1074
+ }
1075
+
1076
+ async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
1077
+ try {
1078
+ if (name === "gjc_coordinator_list_sessions") return { ok: true, sessions: await listSessions() };
1079
+ if (name === "gjc_coordinator_register_session") {
1080
+ requireCoordinatorMutation(config, "sessions", args);
1081
+ const sessionId = safeExternalId("session", args.session_id);
1082
+ const cwd = await assertCoordinatorWorkdir(config, args.cwd);
1083
+ const tmuxSession = safeTmuxSessionName(args.tmux_session);
1084
+ const tmuxTarget = safeTmuxTarget(args.tmux_target);
1085
+ const registered = await registerExistingTmuxSession(
1086
+ {
1087
+ sessionId,
1088
+ cwd,
1089
+ tmuxSession,
1090
+ tmuxTarget,
1091
+ visible: args.visible !== false,
1092
+ warpAttached: optionalBoolean(args.warp_attached),
1093
+ source: optionalString(args.source) ?? "register_session",
1094
+ model: optionalString(args.model),
1095
+ },
1096
+ namespaceDir,
1097
+ sessionFile(sessionId),
1098
+ commandRunner,
1099
+ );
1100
+ return {
1101
+ ok: true,
1102
+ session: registered.session,
1103
+ session_state: registered.sessionState,
1104
+ registered: true,
1105
+ };
1106
+ }
1107
+ if (name === "gjc_coordinator_read_status") {
1108
+ const sessionId = args.session_id;
1109
+ if (sessionId) {
1110
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
1111
+ return {
1112
+ ok: true,
1113
+ session,
1114
+ status: session ? await inspectTmuxSession(session, 80, commandRunner) : { live: false },
1115
+ session_state: await readSessionState(namespaceDir, safeExternalId("session", sessionId)),
1116
+ };
1117
+ }
1118
+ const sessions = await listSessions();
1119
+ const statuses = await Promise.all(
1120
+ sessions.map(async session =>
1121
+ typeof session === "object" && session !== null
1122
+ ? {
1123
+ session,
1124
+ status: await inspectTmuxSession(session as Record<string, unknown>, 40, commandRunner),
1125
+ }
1126
+ : { session, status: { live: null } },
1127
+ ),
1128
+ );
1129
+ return { ok: true, sessions, statuses };
1130
+ }
1131
+ if (name === "gjc_coordinator_read_tail") {
1132
+ const session = asRecord(await readJsonFile(sessionFile(args.session_id)));
1133
+ return { ok: true, lines: session ? await captureTmuxTail(session, boundedLineCount(args.lines)) : [] };
1134
+ }
1135
+ if (name === "gjc_coordinator_list_questions") return { ok: true, questions: await listQuestions(args) };
1136
+ if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
1137
+ if (name === "gjc_coordinator_read_artifact")
1138
+ return await readCoordinatorArtifact(config, { path: args.path });
1139
+ if (name === "gjc_coordinator_read_coordination_status")
1140
+ return { ok: true, reports: await listJsonFiles(path.join(namespaceDir, "reports")) };
1141
+ if (name === "gjc_coordinator_start_session") {
1142
+ requireCoordinatorMutation(config, "sessions", args);
1143
+ const cwd = await assertCoordinatorWorkdir(config, args.cwd);
1144
+ const input = {
1145
+ cwd,
1146
+ prompt: typeof args.prompt === "string" ? args.prompt : undefined,
1147
+ namespace: config.namespace,
1148
+ worktree: true as const,
1149
+ };
1150
+ const started = services.startSession
1151
+ ? await services.startSession(input)
1152
+ : await startTmuxSession(config, input, namespaceDir);
1153
+ const startedRecord = asRecord(started);
1154
+ if (!startedRecord) throw new Error("coordinator_session_command_required");
1155
+ const session = normalizeSession(startedRecord);
1156
+ await writeJsonFile(sessionFile(session.session_id), session);
1157
+ const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
1158
+ let sessionState = await writeSessionState(
1159
+ namespaceDir,
1160
+ String(session.session_id),
1161
+ input.prompt ? "running" : "ready_for_input",
1162
+ { live, reason: null },
1163
+ );
1164
+ if (typeof args.prompt === "string" && args.prompt.length > 0) {
1165
+ const turn = await activateTurn(
1166
+ session,
1167
+ makeTurnRecord(config, String(session.session_id), args.prompt, "active"),
1168
+ );
1169
+ sessionState = (await readSessionState(namespaceDir, turn.session_id)) ?? sessionState;
1170
+ const prompt = {
1171
+ session_id: session.session_id,
1172
+ turn_id: turn.turn_id,
1173
+ prompt: args.prompt,
1174
+ queued: turn.delivery.queued,
1175
+ delivered: turn.delivery.delivered,
1176
+ tmux_keys_sent: turn.delivery.tmux_keys_sent ?? false,
1177
+ prompt_acknowledged: turn.delivery.prompt_acknowledged ?? false,
1178
+ created_at: turn.created_at,
1179
+ };
1180
+ await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`), prompt);
1181
+ return {
1182
+ ok: true,
1183
+ session,
1184
+ session_state: sessionState,
1185
+ turn,
1186
+ turn_id: turn.turn_id,
1187
+ active_turn_id: turn.turn_id,
1188
+ status: turn.status,
1189
+ queued: turn.delivery.queued,
1190
+ delivered: turn.delivery.delivered,
1191
+ delivery: turn.delivery,
1192
+ };
1193
+ }
1194
+ return { ok: true, session, session_state: sessionState };
1195
+ }
1196
+ if (name === "gjc_coordinator_send_prompt") {
1197
+ requireCoordinatorMutation(config, "sessions", args);
1198
+ const sessionId = safeExternalId("session", args.session_id);
1199
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
1200
+ if (!session) return { ok: false, reason: "unknown_session", session_id: sessionId };
1201
+ if (typeof args.prompt !== "string" || args.prompt.length === 0)
1202
+ return { ok: false, reason: "prompt_required" };
1203
+ const activeTurn = await readActiveTurn(namespaceDir, sessionId);
1204
+ if (activeTurn && args.force !== true && args.queue !== true) {
1205
+ return {
1206
+ ok: false,
1207
+ reason: "active_turn_exists",
1208
+ session_id: sessionId,
1209
+ active_turn_id: activeTurn.turn_id,
1210
+ };
1211
+ }
1212
+ if (activeTurn && args.force === true) {
1213
+ const timestamp = new Date().toISOString();
1214
+ const superseded = {
1215
+ ...activeTurn,
1216
+ status: "superseded" as const,
1217
+ updated_at: timestamp,
1218
+ completed_at: timestamp,
1219
+ };
1220
+ await writeTurnRecord(namespaceDir, superseded);
1221
+ await clearActiveTurn(namespaceDir, superseded);
1222
+ }
1223
+ const shouldQueue = args.queue === true && args.force !== true;
1224
+ const turn = shouldQueue
1225
+ ? makeTurnRecord(config, sessionId, args.prompt, "queued")
1226
+ : await activateTurn(session, makeTurnRecord(config, sessionId, args.prompt, "active"));
1227
+ if (shouldQueue) await writeTurnRecord(namespaceDir, turn);
1228
+ const recordedTurn = turn;
1229
+ const prompt = {
1230
+ session_id: sessionId,
1231
+ turn_id: recordedTurn.turn_id,
1232
+ prompt: args.prompt,
1233
+ queued: recordedTurn.delivery.queued,
1234
+ delivered: recordedTurn.delivery.delivered,
1235
+ tmux_keys_sent: recordedTurn.delivery.tmux_keys_sent ?? false,
1236
+ prompt_acknowledged: recordedTurn.delivery.prompt_acknowledged ?? false,
1237
+ created_at: recordedTurn.created_at,
1238
+ };
1239
+ await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`), prompt);
1240
+ return {
1241
+ ok: true,
1242
+ session_id: sessionId,
1243
+ turn_id: recordedTurn.turn_id,
1244
+ active_turn_id: shouldQueue ? activeTurn?.turn_id : recordedTurn.turn_id,
1245
+ status: recordedTurn.status,
1246
+ queued: recordedTurn.delivery.queued,
1247
+ delivered: recordedTurn.delivery.delivered,
1248
+ delivery: recordedTurn.delivery,
1249
+ prompt,
1250
+ tmux_keys_sent: recordedTurn.delivery.tmux_keys_sent ?? false,
1251
+ prompt_acknowledged: recordedTurn.delivery.prompt_acknowledged ?? false,
1252
+ session_state: await readSessionState(namespaceDir, sessionId),
1253
+ };
1254
+ }
1255
+ if (name === "gjc_coordinator_read_turn") {
1256
+ return await readTurnPayload(args.turn_id, args.session_id, args.lines);
1257
+ }
1258
+ if (name === "gjc_coordinator_await_turn") {
1259
+ const timeoutMs = boundedTimeoutMs(args.timeout_ms);
1260
+ const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
1261
+ const deadline = Date.now() + timeoutMs;
1262
+ let payload = await readTurnPayload(args.turn_id, args.session_id, args.lines);
1263
+ while (
1264
+ payload.ok === true &&
1265
+ !TERMINAL_TURN_STATUSES.has((payload.turn as TurnRecord).status) &&
1266
+ Date.now() < deadline
1267
+ ) {
1268
+ const remainingMs = deadline - Date.now();
1269
+ await waitForTurnStateChange(
1270
+ namespaceDir,
1271
+ payload.turn as TurnRecord,
1272
+ Math.min(pollIntervalMs, remainingMs),
1273
+ );
1274
+ payload = await readTurnPayload(args.turn_id, args.session_id, args.lines);
1275
+ }
1276
+ if (payload.ok === true && !TERMINAL_TURN_STATUSES.has((payload.turn as TurnRecord).status)) {
1277
+ return {
1278
+ ok: false,
1279
+ reason: "timeout",
1280
+ turn: payload.turn,
1281
+ advisory_status: payload.advisory_status,
1282
+ session_state: payload.session_state,
1283
+ };
1284
+ }
1285
+ return payload;
1286
+ }
1287
+ if (name === "gjc_coordinator_submit_question_answer") {
1288
+ requireCoordinatorMutation(config, "questions", args);
1289
+ const questionId = safeExternalId("question", args.question_id);
1290
+ const questionPath = questionFile(namespaceDir, questionId);
1291
+ const question = asRecord(await readJsonFile(questionPath));
1292
+ if (!question) return { ok: false, reason: "unknown_question" };
1293
+ if (args.session_id != null && question.session_id !== safeExternalId("session", args.session_id)) {
1294
+ return { ok: false, reason: "question_session_mismatch" };
1295
+ }
1296
+ if (args.turn_id != null && question.turn_id !== safeTurnId(args.turn_id)) {
1297
+ return { ok: false, reason: "question_turn_mismatch" };
1298
+ }
1299
+ const answeredTurnId = typeof question.turn_id === "string" ? question.turn_id : null;
1300
+ const answered = {
1301
+ ...question,
1302
+ status: "answered",
1303
+ answer: args.answer,
1304
+ answered_at: new Date().toISOString(),
1305
+ };
1306
+ await writeJsonFile(questionPath, answered);
1307
+ let turn: TurnRecord | null = null;
1308
+ if (answeredTurnId) {
1309
+ turn = await readTurnRecord(namespaceDir, answeredTurnId);
1310
+ if (turn) {
1311
+ const timestamp = new Date().toISOString();
1312
+ turn = {
1313
+ ...turn,
1314
+ status: "active",
1315
+ question_ids: [...new Set([...turn.question_ids, questionId])],
1316
+ updated_at: timestamp,
1317
+ };
1318
+ await writeTurnRecord(namespaceDir, turn);
1319
+ await writeActiveTurn(namespaceDir, turn);
1320
+ await writeSessionState(namespaceDir, turn.session_id, "running", {
1321
+ currentTurnId: turn.turn_id,
1322
+ live: null,
1323
+ reason: null,
1324
+ });
1325
+ const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
1326
+ if (session && typeof args.answer === "string")
1327
+ await sendTmuxPrompt(session, args.answer, commandRunner);
1328
+ }
1329
+ }
1330
+ return { ok: true, question: answered, ...(turn ? { turn } : {}) };
1331
+ }
1332
+ if (name === "gjc_coordinator_report_status") {
1333
+ requireCoordinatorMutation(config, "reports", args);
1334
+ const evidence = await validateEvidencePaths(args.evidence_paths);
1335
+ const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
1336
+ const report = {
1337
+ session_id: sessionId,
1338
+ turn_id: args.turn_id,
1339
+ status: args.status,
1340
+ summary: args.summary,
1341
+ blocker: args.blocker,
1342
+ pr_url: args.pr_url,
1343
+ evidence_paths: evidence.map(item => item.path),
1344
+ created_at: new Date().toISOString(),
1345
+ };
1346
+ let turn: TurnRecord | null = null;
1347
+ let promotedTurn: TurnRecord | null = null;
1348
+ if (args.turn_id != null) {
1349
+ turn = await readTurnRecord(namespaceDir, args.turn_id);
1350
+ if (!turn) return { ok: false, reason: "unknown_turn" };
1351
+ if (sessionId != null && turn.session_id !== sessionId) {
1352
+ return { ok: false, reason: "turn_session_mismatch" };
1353
+ }
1354
+ const terminalStatus = asTerminalTurnStatus(args.status);
1355
+ if (terminalStatus) {
1356
+ const timestamp = new Date().toISOString();
1357
+ turn = {
1358
+ ...turn,
1359
+ status: terminalStatus,
1360
+ delivery: {
1361
+ ...turn.delivery,
1362
+ prompt_acknowledged: true,
1363
+ state: "acknowledged",
1364
+ },
1365
+ final_response: {
1366
+ text:
1367
+ typeof args.summary === "string"
1368
+ ? args.summary
1369
+ : typeof args.blocker === "string"
1370
+ ? args.blocker
1371
+ : null,
1372
+ format: "markdown",
1373
+ source: "report_status",
1374
+ artifact_path: null,
1375
+ truncated: false,
1376
+ },
1377
+ evidence,
1378
+ error:
1379
+ terminalStatus === "failed"
1380
+ ? {
1381
+ code: "reported_failure",
1382
+ message:
1383
+ typeof args.blocker === "string" ? args.blocker : String(args.summary ?? "failed"),
1384
+ recoverable: true,
1385
+ }
1386
+ : null,
1387
+ updated_at: timestamp,
1388
+ completed_at: timestamp,
1389
+ };
1390
+ await writeTurnRecord(namespaceDir, turn);
1391
+ await clearActiveTurn(namespaceDir, turn);
1392
+ await writeSessionState(
1393
+ namespaceDir,
1394
+ turn.session_id,
1395
+ terminalStatus === "failed" ? "errored" : "completed",
1396
+ {
1397
+ lastTurnId: turn.turn_id,
1398
+ live: null,
1399
+ reason: terminalStatus === "failed" ? "reported_failure" : null,
1400
+ },
1401
+ );
1402
+ promotedTurn = await promoteNextQueuedTurn(turn.session_id);
1403
+ }
1404
+ }
1405
+ await writeJsonFile(path.join(namespaceDir, "reports", `${Date.now()}.json`), report);
1406
+ return {
1407
+ ok: true,
1408
+ report,
1409
+ ...(turn ? { turn, session_state: await readSessionState(namespaceDir, turn.session_id) } : {}),
1410
+ ...(promotedTurn ? { promoted_turn: promotedTurn } : {}),
1411
+ };
1412
+ }
1413
+ return { ok: false, reason: "unknown_tool", tool: name };
1414
+ } catch (error) {
1415
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
1416
+ }
1417
+ }
1418
+
1419
+ async function handleJsonRpc(request: JsonRpcRequest): Promise<JsonRpcResponse> {
1420
+ const id = request.id ?? null;
1421
+ if (request.method === "initialize") {
1422
+ return {
1423
+ jsonrpc: "2.0",
1424
+ id,
1425
+ result: {
1426
+ protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION,
1427
+ capabilities: { tools: {}, prompts: {}, resources: {} },
1428
+ serverInfo: { name: COORDINATOR_MCP_SERVER_NAME, version: VERSION },
1429
+ },
1430
+ };
1431
+ }
1432
+ if (request.method === "tools/list") {
1433
+ return { jsonrpc: "2.0", id, result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(toolSchema) } };
1434
+ }
1435
+ if (request.method === "prompts/list") {
1436
+ return { jsonrpc: "2.0", id, result: { prompts: [] } };
1437
+ }
1438
+ if (request.method === "resources/list") {
1439
+ return { jsonrpc: "2.0", id, result: { resources: [] } };
1440
+ }
1441
+ if (request.method === "tools/call") {
1442
+ const params = (request.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };
1443
+ const payload = await callTool(params.name ?? "", params.arguments ?? {});
1444
+ return { jsonrpc: "2.0", id, result: textResult(payload, payload.ok === false) };
1445
+ }
1446
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: `unknown_method:${request.method}` } };
1447
+ }
1448
+
1449
+ return { config, callTool, handleJsonRpc, handle: handleJsonRpc };
1450
+ }
1451
+
1452
+ function legacyToolResult(payload: unknown): { content: Array<{ type: "text"; text: string }>; isError: boolean } {
1453
+ const failed = typeof payload === "object" && payload !== null && (payload as { ok?: unknown }).ok === false;
1454
+ return textResult(payload, failed);
1455
+ }
1456
+
1457
+ export async function handleCoordinatorMcpRequest(
1458
+ request: JsonRpcRequest,
1459
+ options: LegacyHandlerOptions = {},
1460
+ ): Promise<JsonRpcResponse> {
1461
+ if (request.method === "initialize") {
1462
+ return {
1463
+ jsonrpc: "2.0",
1464
+ id: request.id ?? null,
1465
+ result: {
1466
+ protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION,
1467
+ capabilities: { tools: {}, prompts: {}, resources: {} },
1468
+ serverInfo: { name: COORDINATOR_MCP_SERVER_NAME, version: VERSION },
1469
+ },
1470
+ };
1471
+ }
1472
+ if (request.method === "tools/list") {
1473
+ return { jsonrpc: "2.0", id: request.id ?? null, result: { tools: COORDINATOR_MCP_TOOL_NAMES.map(toolSchema) } };
1474
+ }
1475
+ if (request.method === "prompts/list") {
1476
+ return { jsonrpc: "2.0", id: request.id ?? null, result: { prompts: [] } };
1477
+ }
1478
+ if (request.method === "resources/list") {
1479
+ return { jsonrpc: "2.0", id: request.id ?? null, result: { resources: [] } };
1480
+ }
1481
+ if (request.method !== "tools/call")
1482
+ return {
1483
+ jsonrpc: "2.0",
1484
+ id: request.id ?? null,
1485
+ error: { code: -32601, message: `unknown_method:${request.method}` },
1486
+ };
1487
+ const params = (request.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };
1488
+ const args = params.arguments ?? {};
1489
+ const server = createCoordinatorMcpServer({
1490
+ env: options.env ?? process.env,
1491
+ services: options.createSession ? { startSession: () => options.createSession?.() } : undefined,
1492
+ });
1493
+ return {
1494
+ jsonrpc: "2.0",
1495
+ id: request.id ?? null,
1496
+ result: legacyToolResult(await server.callTool(params.name ?? "", args)),
1497
+ };
1498
+ }
1499
+
1500
+ export async function runCoordinatorMcpStdio(options: CoordinatorMcpServerOptions = {}): Promise<void> {
1501
+ const server = createCoordinatorMcpServer(options);
1502
+ let buffer = "";
1503
+ for await (const chunk of process.stdin) {
1504
+ buffer += chunk.toString();
1505
+ let newline = buffer.indexOf("\n");
1506
+ while (newline >= 0) {
1507
+ const line = buffer.slice(0, newline).trim();
1508
+ buffer = buffer.slice(newline + 1);
1509
+ if (line.length > 0) {
1510
+ const request = JSON.parse(line) as JsonRpcRequest;
1511
+ if (request.id !== undefined && request.id !== null) {
1512
+ const response = await server.handleJsonRpc(request);
1513
+ process.stdout.write(`${JSON.stringify(response)}\n`);
1514
+ }
1515
+ }
1516
+ newline = buffer.indexOf("\n");
1517
+ }
1518
+ }
1519
+ }