@gajae-code/coding-agent 0.4.4 → 0.5.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 (132) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/types/cli/fast-help.d.ts +1 -0
  3. package/dist/types/cli/setup-cli.d.ts +2 -0
  4. package/dist/types/commands/harness.d.ts +6 -0
  5. package/dist/types/commands/setup.d.ts +6 -0
  6. package/dist/types/config/model-profile-activation.d.ts +11 -2
  7. package/dist/types/config/model-profiles.d.ts +7 -0
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/model-resolver.d.ts +2 -0
  10. package/dist/types/config/models-config-schema.d.ts +35 -0
  11. package/dist/types/config/settings-schema.d.ts +4 -3
  12. package/dist/types/coordinator/contract.d.ts +1 -1
  13. package/dist/types/coordinator-mcp/server.d.ts +8 -2
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
  16. package/dist/types/harness-control-plane/finalize.d.ts +5 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +1 -1
  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/receipt-spool.d.ts +19 -0
  21. package/dist/types/harness-control-plane/receipts.d.ts +46 -0
  22. package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
  23. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  24. package/dist/types/harness-control-plane/types.d.ts +13 -1
  25. package/dist/types/hindsight/mental-models.d.ts +5 -5
  26. package/dist/types/main.d.ts +2 -2
  27. package/dist/types/modes/components/model-selector.d.ts +1 -12
  28. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  29. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  30. package/dist/types/modes/utils/abort-message.d.ts +4 -0
  31. package/dist/types/sdk.d.ts +5 -0
  32. package/dist/types/session/agent-session.d.ts +2 -0
  33. package/dist/types/session/blob-store.d.ts +20 -1
  34. package/dist/types/session/session-manager.d.ts +32 -6
  35. package/dist/types/session/streaming-output.d.ts +3 -2
  36. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  37. package/dist/types/setup/hermes-setup.d.ts +7 -0
  38. package/dist/types/task/fork-context-advisory.d.ts +13 -0
  39. package/dist/types/task/receipt.d.ts +2 -0
  40. package/dist/types/task/roi-reconciliation.d.ts +27 -0
  41. package/dist/types/task/types.d.ts +17 -0
  42. package/dist/types/thinking-metadata.d.ts +16 -0
  43. package/dist/types/thinking.d.ts +3 -12
  44. package/dist/types/tools/index.d.ts +2 -0
  45. package/dist/types/tools/resolve.d.ts +0 -10
  46. package/dist/types/utils/tool-choice.d.ts +14 -1
  47. package/package.json +8 -7
  48. package/scripts/build-binary.ts +4 -0
  49. package/src/cli/fast-help.ts +80 -0
  50. package/src/cli/setup-cli.ts +12 -3
  51. package/src/cli.ts +112 -17
  52. package/src/commands/coordinator.ts +44 -1
  53. package/src/commands/harness.ts +128 -11
  54. package/src/commands/launch.ts +2 -2
  55. package/src/commands/mcp-serve.ts +3 -2
  56. package/src/commands/session.ts +3 -1
  57. package/src/commands/setup.ts +4 -0
  58. package/src/config/model-profile-activation.ts +15 -3
  59. package/src/config/model-profiles.ts +255 -56
  60. package/src/config/model-resolver.ts +9 -6
  61. package/src/config/models-config-schema.ts +2 -0
  62. package/src/config/settings-schema.ts +6 -3
  63. package/src/coordinator/contract.ts +1 -0
  64. package/src/coordinator-mcp/server.ts +427 -193
  65. package/src/cursor.ts +46 -4
  66. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  67. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  68. package/src/export/html/index.ts +13 -9
  69. package/src/gjc-runtime/launch-worktree.ts +12 -1
  70. package/src/gjc-runtime/session-state-sidecar.ts +38 -0
  71. package/src/gjc-runtime/team-runtime.ts +33 -7
  72. package/src/gjc-runtime/tmux-common.ts +15 -0
  73. package/src/gjc-runtime/tmux-sessions.ts +19 -11
  74. package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
  75. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  76. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  77. package/src/harness-control-plane/finalize.ts +39 -5
  78. package/src/harness-control-plane/owner.ts +87 -28
  79. package/src/harness-control-plane/phase-rollup.ts +96 -0
  80. package/src/harness-control-plane/receipt-ingest.ts +127 -0
  81. package/src/harness-control-plane/receipt-spool.ts +128 -0
  82. package/src/harness-control-plane/receipts.ts +229 -1
  83. package/src/harness-control-plane/rpc-adapter.ts +8 -0
  84. package/src/harness-control-plane/state-machine.ts +27 -6
  85. package/src/harness-control-plane/storage.ts +23 -0
  86. package/src/harness-control-plane/types.ts +33 -1
  87. package/src/hindsight/mental-models.ts +17 -16
  88. package/src/internal-urls/docs-index.generated.ts +8 -7
  89. package/src/main.ts +7 -3
  90. package/src/modes/components/assistant-message.ts +26 -14
  91. package/src/modes/components/diff.ts +97 -0
  92. package/src/modes/components/model-selector.ts +353 -181
  93. package/src/modes/components/status-line.ts +6 -6
  94. package/src/modes/components/tool-execution.ts +30 -13
  95. package/src/modes/controllers/event-controller.ts +5 -4
  96. package/src/modes/controllers/selector-controller.ts +33 -42
  97. package/src/modes/interactive-mode.ts +4 -5
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +3 -2
  100. package/src/modes/rpc/rpc-mode.ts +44 -14
  101. package/src/modes/rpc/rpc-types.ts +5 -2
  102. package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
  103. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  104. package/src/modes/theme/theme.ts +2 -2
  105. package/src/modes/utils/abort-message.ts +41 -0
  106. package/src/modes/utils/context-usage.ts +15 -8
  107. package/src/modes/utils/ui-helpers.ts +5 -6
  108. package/src/sdk.ts +38 -6
  109. package/src/secrets/obfuscator.ts +102 -27
  110. package/src/session/agent-session.ts +121 -25
  111. package/src/session/blob-store.ts +89 -3
  112. package/src/session/session-manager.ts +328 -57
  113. package/src/session/streaming-output.ts +185 -122
  114. package/src/session/tool-choice-queue.ts +23 -0
  115. package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
  116. package/src/setup/hermes-setup.ts +63 -8
  117. package/src/task/executor.ts +69 -6
  118. package/src/task/fork-context-advisory.ts +99 -0
  119. package/src/task/index.ts +31 -2
  120. package/src/task/receipt.ts +7 -0
  121. package/src/task/render.ts +21 -1
  122. package/src/task/roi-reconciliation.ts +90 -0
  123. package/src/task/types.ts +15 -0
  124. package/src/thinking-metadata.ts +51 -0
  125. package/src/thinking.ts +26 -46
  126. package/src/tools/bash.ts +1 -1
  127. package/src/tools/index.ts +4 -2
  128. package/src/tools/resolve.ts +93 -18
  129. package/src/tools/subagent-render.ts +10 -1
  130. package/src/utils/edit-mode.ts +1 -1
  131. package/src/utils/title-generator.ts +16 -2
  132. package/src/utils/tool-choice.ts +45 -16
@@ -32,10 +32,12 @@ interface JsonRpcRequest {
32
32
  params?: unknown;
33
33
  }
34
34
 
35
+ type JsonRpcResult = any;
36
+
35
37
  interface JsonRpcResponse {
36
38
  jsonrpc: "2.0";
37
39
  id: string | number | null;
38
- result?: any;
40
+ result?: JsonRpcResult;
39
41
  error?: { code: number; message: string; data?: unknown };
40
42
  }
41
43
 
@@ -46,9 +48,41 @@ interface SessionStartInput {
46
48
  worktree: true;
47
49
  }
48
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
+
49
82
  interface CoordinatorServices {
50
83
  listSessions?: () => unknown[] | Promise<unknown[]>;
51
84
  startSession?: (input: SessionStartInput) => unknown | Promise<unknown>;
85
+ commandRunner?: (command: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>;
52
86
  }
53
87
 
54
88
  interface CoordinatorMcpServerOptions {
@@ -134,10 +168,16 @@ interface CoordinatorSessionState {
134
168
  reason: string | null;
135
169
  }
136
170
 
171
+ const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
137
172
  const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
138
173
  const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
139
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;
140
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
+ }
141
181
 
142
182
  function textResult(
143
183
  payload: unknown,
@@ -162,6 +202,27 @@ function toolSchema(name: CoordinatorToolName): {
162
202
  const sessionId = { type: "string", description: "GJC coordinator bridge session id." };
163
203
  const pathField = { type: "string", description: "Artifact path inside configured safe roots." };
164
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
+ }
165
226
  if (name === "gjc_coordinator_start_session") {
166
227
  return {
167
228
  name,
@@ -293,7 +354,7 @@ function toolSchema(name: CoordinatorToolName): {
293
354
  return { name, description: "List known scoped GJC coordinator bridge sessions.", inputSchema: common };
294
355
  }
295
356
 
296
- function normalizeSession(session: any): Record<string, unknown> {
357
+ function normalizeSession(session: Record<string, unknown>): Record<string, unknown> {
297
358
  return {
298
359
  session_id: session.sessionId ?? session.session_id ?? session.name ?? "unknown",
299
360
  ...(session.tmuxSession ? { tmux_session: session.tmuxSession } : {}),
@@ -307,7 +368,7 @@ async function ensureDir(dir: string): Promise<void> {
307
368
  await fs.mkdir(dir, { recursive: true });
308
369
  }
309
370
 
310
- async function readJsonFile(file: string): Promise<any | null> {
371
+ async function readJsonFile(file: string): Promise<unknown | null> {
311
372
  try {
312
373
  return JSON.parse(await fs.readFile(file, "utf8"));
313
374
  } catch {
@@ -320,7 +381,7 @@ async function writeJsonFile(file: string, value: unknown): Promise<void> {
320
381
  await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
321
382
  }
322
383
 
323
- async function listJsonFiles(dir: string): Promise<any[]> {
384
+ async function listJsonFiles(dir: string): Promise<unknown[]> {
324
385
  try {
325
386
  const entries = await fs.readdir(dir);
326
387
  const values = await Promise.all(
@@ -342,6 +403,28 @@ function safeTurnId(value: unknown): string {
342
403
  return value;
343
404
  }
344
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
+
345
428
  function turnsDir(namespaceDir: string): string {
346
429
  return path.join(namespaceDir, "turns");
347
430
  }
@@ -371,7 +454,7 @@ async function writeTurnRecord(namespaceDir: string, turn: TurnRecord): Promise<
371
454
  }
372
455
 
373
456
  async function readActiveTurn(namespaceDir: string, sessionId: string): Promise<TurnRecord | null> {
374
- const active = await readJsonFile(activeTurnFile(namespaceDir, sessionId));
457
+ const active = asRecord(await readJsonFile(activeTurnFile(namespaceDir, sessionId)));
375
458
  if (!active || typeof active.turn_id !== "string") return null;
376
459
  const turn = await readTurnRecord(namespaceDir, active.turn_id);
377
460
  if (!turn || turn.session_id !== sessionId || !ACTIVE_TURN_STATUSES.has(turn.status)) return null;
@@ -388,7 +471,7 @@ async function writeActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<
388
471
  }
389
472
 
390
473
  async function clearActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
391
- const active = await readJsonFile(activeTurnFile(namespaceDir, turn.session_id));
474
+ const active = asRecord(await readJsonFile(activeTurnFile(namespaceDir, turn.session_id)));
392
475
  if (active?.turn_id === turn.turn_id) await fs.rm(activeTurnFile(namespaceDir, turn.session_id), { force: true });
393
476
  }
394
477
 
@@ -469,6 +552,14 @@ async function markTurnTerminalFromSessionState(
469
552
  sessionState: CoordinatorSessionState,
470
553
  ): Promise<TurnRecord> {
471
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
+ };
472
563
  const timestamp = new Date().toISOString();
473
564
  const resolved: TurnRecord = {
474
565
  ...turn,
@@ -478,16 +569,24 @@ async function markTurnTerminalFromSessionState(
478
569
  prompt_acknowledged: true,
479
570
  state: "acknowledged",
480
571
  },
481
- final_response: {
482
- text: null,
483
- format: "markdown",
484
- source: "runtime_state",
485
- artifact_path: null,
486
- truncated: false,
487
- },
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
+ ],
488
583
  error:
489
584
  terminalStatus === "failed"
490
- ? { code: "runtime_errored", message: sessionState.reason ?? "runtime_errored", recoverable: true }
585
+ ? (runtimeState.error ?? {
586
+ code: "runtime_errored",
587
+ message: sessionState.reason ?? "runtime_errored",
588
+ recoverable: true,
589
+ })
491
590
  : null,
492
591
  updated_at: timestamp,
493
592
  completed_at: timestamp,
@@ -505,7 +604,6 @@ async function markTurnTerminalFromSessionState(
505
604
  function shellQuote(value: string): string {
506
605
  return `'${value.replaceAll("'", "'\\''")}'`;
507
606
  }
508
-
509
607
  function makeTurnRecord(
510
608
  config: CoordinatorMcpConfig,
511
609
  sessionId: string,
@@ -571,8 +669,14 @@ async function runCommand(command: string[]): Promise<{ exitCode: number; stdout
571
669
  return { exitCode, stdout, stderr };
572
670
  }
573
671
 
574
- async function sendTmuxPromptKeys(target: string, prompt: string): Promise<boolean> {
575
- const sent = await runCommand(["tmux", "send-keys", "-t", target, prompt, "C-m", "C-m"]);
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"]);
576
680
  return sent.exitCode === 0;
577
681
  }
578
682
 
@@ -582,12 +686,65 @@ function boundedLineCount(value: unknown): number {
582
686
  return Math.min(parsed, 400);
583
687
  }
584
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
+
585
741
  async function startTmuxSession(
586
742
  config: CoordinatorMcpConfig,
587
743
  input: SessionStartInput,
588
744
  namespaceDir: string,
589
- ): Promise<Record<string, unknown> | null> {
590
- if (!config.sessionCommand) return null;
745
+ runner: CommandRunner = runCommand,
746
+ ): Promise<Record<string, unknown>> {
747
+ if (!config.sessionCommand) throw new Error("coordinator_session_command_required");
591
748
  const sessionName = `gjc-coordinator-${randomUUID().slice(0, 8)}`;
592
749
  const runtimeStateFile = sessionStateFile(namespaceDir, sessionName);
593
750
  const sessionCommand = [
@@ -596,7 +753,7 @@ async function startTmuxSession(
596
753
  `${GJC_COORDINATOR_SESSION_ID_ENV}=${shellQuote(sessionName)}`,
597
754
  config.sessionCommand,
598
755
  ].join(" ");
599
- const started = await runCommand([
756
+ const started = await runner([
600
757
  "tmux",
601
758
  "new-session",
602
759
  "-d",
@@ -611,9 +768,6 @@ async function startTmuxSession(
611
768
  ]);
612
769
  if (started.exitCode !== 0) throw new Error(`coordinator_tmux_start_failed:${started.stderr || started.stdout}`);
613
770
  const [tmuxTarget, paneId] = started.stdout.trim().split(/\s+/, 2);
614
- const initialPromptTmuxKeysSent = input.prompt
615
- ? await sendTmuxPromptKeys(tmuxTarget || sessionName, input.prompt)
616
- : false;
617
771
  return {
618
772
  sessionId: sessionName,
619
773
  tmuxSession: sessionName,
@@ -623,7 +777,6 @@ async function startTmuxSession(
623
777
  createdAt: new Date().toISOString(),
624
778
  sessionCommand: config.sessionCommand,
625
779
  runtimeStateFile,
626
- initialPromptTmuxKeysSent,
627
780
  };
628
781
  }
629
782
 
@@ -635,16 +788,23 @@ async function captureTmuxTail(session: Record<string, unknown>, lines: number):
635
788
  return captured.stdout.split("\n").slice(-lines);
636
789
  }
637
790
 
638
- async function sendTmuxPrompt(session: Record<string, unknown>, prompt: string): Promise<boolean> {
791
+ async function sendTmuxPrompt(
792
+ session: Record<string, unknown>,
793
+ prompt: string,
794
+ runner: CommandRunner = runCommand,
795
+ ): Promise<boolean> {
639
796
  const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
640
797
  if (typeof target !== "string" || target.length === 0) return false;
641
- return await sendTmuxPromptKeys(target, prompt);
798
+ return await sendTmuxPromptKeys(target, prompt, runner);
642
799
  }
643
800
 
644
- async function hasTmuxSession(session: Record<string, unknown>): Promise<boolean | null> {
801
+ async function hasTmuxSession(
802
+ session: Record<string, unknown>,
803
+ runner: CommandRunner = runCommand,
804
+ ): Promise<boolean | null> {
645
805
  const tmuxSession = typeof session.tmux_session === "string" ? session.tmux_session : session.tmuxSession;
646
806
  if (typeof tmuxSession !== "string" || tmuxSession.length === 0) return null;
647
- const checked = await runCommand(["tmux", "has-session", "-t", tmuxSession]);
807
+ const checked = await runner(["tmux", "has-session", "-t", tmuxSession]);
648
808
  return checked.exitCode === 0;
649
809
  }
650
810
 
@@ -673,8 +833,12 @@ function summarizePaneTail(lines: string[]): Record<string, unknown> {
673
833
  };
674
834
  }
675
835
 
676
- async function inspectTmuxSession(session: Record<string, unknown>, lines = 80): Promise<Record<string, unknown>> {
677
- const live = await hasTmuxSession(session);
836
+ async function inspectTmuxSession(
837
+ session: Record<string, unknown>,
838
+ lines = 80,
839
+ runner: CommandRunner = runCommand,
840
+ ): Promise<Record<string, unknown>> {
841
+ const live = await hasTmuxSession(session, runner);
678
842
  const tail = live ? await captureTmuxTail(session, lines) : [];
679
843
  return {
680
844
  live,
@@ -763,6 +927,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
763
927
  const config = buildCoordinatorMcpConfig(options.env ?? process.env);
764
928
  const services = options.services ?? {};
765
929
  const namespaceDir = coordinatorNamespacePath(config);
930
+ const commandRunner = services.commandRunner ?? runCommand;
766
931
 
767
932
  async function listSessions(): Promise<unknown[]> {
768
933
  if (!config.namespace.profile || !config.namespace.repo) return [];
@@ -772,6 +937,114 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
772
937
  function sessionFile(sessionId: unknown): string {
773
938
  return path.join(namespaceDir, "sessions", `${safeExternalId("session", sessionId)}.json`);
774
939
  }
940
+ async function listQuestions(args: Record<string, unknown>): Promise<unknown[]> {
941
+ const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
942
+ const status = typeof args.status === "string" && args.status.length > 0 ? args.status : null;
943
+ return (await listJsonFiles(path.join(namespaceDir, "questions"))).filter(question => {
944
+ const record = asRecord(question);
945
+ if (!record) return false;
946
+ if (sessionId && record.session_id !== sessionId) return false;
947
+ if (status && record.status !== status) return false;
948
+ return true;
949
+ });
950
+ }
951
+
952
+ async function validateEvidencePaths(value: unknown): Promise<Array<{ path: string }>> {
953
+ if (value == null) return [];
954
+ if (!Array.isArray(value)) throw new Error("coordinator_evidence_paths_must_be_array");
955
+ const evidence: Array<{ path: string }> = [];
956
+ for (const item of value) {
957
+ const resolved = await assertCoordinatorArtifactPath(config, item);
958
+ evidence.push({ path: resolved.path });
959
+ }
960
+ return evidence;
961
+ }
962
+
963
+ async function activateTurn(session: Record<string, unknown>, turn: TurnRecord): Promise<TurnRecord> {
964
+ const timestamp = new Date().toISOString();
965
+ const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
966
+ const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
967
+ const pendingTurn: TurnRecord = {
968
+ ...turn,
969
+ status: "active",
970
+ delivery: {
971
+ delivered: false,
972
+ queued: true,
973
+ target: typeof target === "string" ? target : null,
974
+ tmux_keys_sent: false,
975
+ prompt_acknowledged: false,
976
+ state: "queued",
977
+ attempts: [
978
+ {
979
+ delivered: false,
980
+ tmux_keys_sent: false,
981
+ channel: "tmux_keys",
982
+ created_at: timestamp,
983
+ reason: "awaiting_tmux_delivery",
984
+ },
985
+ ],
986
+ },
987
+ liveness: { checked_at: timestamp, live, reason: live === false ? "tmux_session_missing" : null },
988
+ started_at: turn.started_at ?? timestamp,
989
+ updated_at: timestamp,
990
+ };
991
+ await writeTurnRecord(namespaceDir, pendingTurn);
992
+ await writeActiveTurn(namespaceDir, pendingTurn);
993
+ await writeSessionState(namespaceDir, pendingTurn.session_id, "running", {
994
+ currentTurnId: pendingTurn.turn_id,
995
+ live,
996
+ reason: null,
997
+ });
998
+
999
+ const tmuxKeysSent = await sendTmuxPrompt(session, turn.prompt.text, commandRunner);
1000
+ const deliveredAt = new Date().toISOString();
1001
+ const activeTurn: TurnRecord = {
1002
+ ...pendingTurn,
1003
+ delivery: {
1004
+ delivered: false,
1005
+ queued: !tmuxKeysSent,
1006
+ target: typeof target === "string" ? target : null,
1007
+ tmux_keys_sent: tmuxKeysSent,
1008
+ prompt_acknowledged: false,
1009
+ state: tmuxKeysSent ? "tmux_keys_sent" : "unavailable",
1010
+ attempts: [
1011
+ {
1012
+ delivered: false,
1013
+ tmux_keys_sent: tmuxKeysSent,
1014
+ channel: "tmux_keys",
1015
+ created_at: deliveredAt,
1016
+ reason: tmuxKeysSent ? "awaiting_runtime_ack" : "tmux_delivery_unavailable",
1017
+ },
1018
+ ],
1019
+ },
1020
+ updated_at: deliveredAt,
1021
+ };
1022
+ await writeTurnRecord(namespaceDir, activeTurn);
1023
+ await writeActiveTurn(namespaceDir, activeTurn);
1024
+ const sessionState = await readSessionState(namespaceDir, activeTurn.session_id);
1025
+ const runtimeStateAlreadySettled =
1026
+ sessionState?.current_turn_id === activeTurn.turn_id &&
1027
+ (sessionState.state === "completed" || sessionState.state === "errored");
1028
+ if (!runtimeStateAlreadySettled) {
1029
+ await writeSessionState(namespaceDir, activeTurn.session_id, tmuxKeysSent ? "running" : "stale", {
1030
+ currentTurnId: activeTurn.turn_id,
1031
+ live,
1032
+ reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
1033
+ });
1034
+ }
1035
+ return activeTurn;
1036
+ }
1037
+
1038
+ async function promoteNextQueuedTurn(sessionId: string): Promise<TurnRecord | null> {
1039
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
1040
+ if (!session) return null;
1041
+ const queuedTurns = (await listJsonFiles(turnsDir(namespaceDir)))
1042
+ .map(turn => asRecord(turn) as TurnRecord | null)
1043
+ .filter((turn): turn is TurnRecord => turn?.session_id === sessionId && turn.status === "queued")
1044
+ .sort((left, right) => left.created_at.localeCompare(right.created_at));
1045
+ const nextTurn = queuedTurns[0];
1046
+ return nextTurn ? await activateTurn(session, nextTurn) : null;
1047
+ }
775
1048
 
776
1049
  async function readTurnPayload(
777
1050
  turnId: unknown,
@@ -783,7 +1056,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
783
1056
  if (sessionId != null && turn.session_id !== safeExternalId("session", sessionId)) {
784
1057
  return { ok: false, reason: "turn_session_mismatch" };
785
1058
  }
786
- const session = await readJsonFile(sessionFile(turn.session_id));
1059
+ const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
787
1060
  let resolvedTurn = turn;
788
1061
  let advisoryStatus: Record<string, unknown> = { live: false };
789
1062
  let sessionState = await readSessionState(namespaceDir, turn.session_id);
@@ -811,77 +1084,88 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
811
1084
  resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "session_record_missing");
812
1085
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
813
1086
  } else if (session) {
814
- advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines));
1087
+ advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines), commandRunner);
815
1088
  if (ACTIVE_TURN_STATUSES.has(turn.status) && hasTmuxIdentity(session) && advisoryStatus.live === false) {
816
1089
  resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "tmux_session_missing");
817
1090
  sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
818
1091
  }
819
1092
  }
1093
+ const missingFinalResponse =
1094
+ resolvedTurn.status === "completed" && !reportableFinalResponse(resolvedTurn.final_response);
820
1095
  return {
821
1096
  ok: true,
822
1097
  turn: resolvedTurn,
823
1098
  advisory_status: advisoryStatus,
824
1099
  session_state: sessionState,
1100
+ ...(missingFinalResponse
1101
+ ? {
1102
+ completion_missing_final_response: true,
1103
+ advisory: MISSING_FINAL_RESPONSE_ADVISORY,
1104
+ }
1105
+ : {}),
825
1106
  };
826
1107
  }
827
1108
 
828
1109
  async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
829
1110
  try {
830
1111
  if (name === "gjc_coordinator_list_sessions") return { ok: true, sessions: await listSessions() };
1112
+ if (name === "gjc_coordinator_register_session") {
1113
+ requireCoordinatorMutation(config, "sessions", args);
1114
+ const sessionId = safeExternalId("session", args.session_id);
1115
+ const cwd = await assertCoordinatorWorkdir(config, args.cwd);
1116
+ const tmuxSession = safeTmuxSessionName(args.tmux_session);
1117
+ const tmuxTarget = safeTmuxTarget(args.tmux_target);
1118
+ const registered = await registerExistingTmuxSession(
1119
+ {
1120
+ sessionId,
1121
+ cwd,
1122
+ tmuxSession,
1123
+ tmuxTarget,
1124
+ visible: args.visible !== false,
1125
+ warpAttached: optionalBoolean(args.warp_attached),
1126
+ source: optionalString(args.source) ?? "register_session",
1127
+ model: optionalString(args.model),
1128
+ },
1129
+ namespaceDir,
1130
+ sessionFile(sessionId),
1131
+ commandRunner,
1132
+ );
1133
+ return {
1134
+ ok: true,
1135
+ session: registered.session,
1136
+ session_state: registered.sessionState,
1137
+ registered: true,
1138
+ };
1139
+ }
831
1140
  if (name === "gjc_coordinator_read_status") {
832
1141
  const sessionId = args.session_id;
833
1142
  if (sessionId) {
834
- const session = await readJsonFile(sessionFile(sessionId));
1143
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
835
1144
  return {
836
1145
  ok: true,
837
1146
  session,
838
- status: session ? await inspectTmuxSession(session) : { live: false },
1147
+ status: session ? await inspectTmuxSession(session, 80, commandRunner) : { live: false },
839
1148
  session_state: await readSessionState(namespaceDir, safeExternalId("session", sessionId)),
840
1149
  };
841
1150
  }
842
1151
  const sessions = await listSessions();
843
1152
  const statuses = await Promise.all(
844
- sessions.map(async session => {
845
- if (typeof session !== "object" || session === null) return { session, status: { live: null } };
846
- const normalized = session as Record<string, unknown>;
847
- const listedSessionId =
848
- typeof normalized.session_id === "string"
849
- ? normalized.session_id
850
- : normalizeSession(normalized).session_id;
851
- return {
852
- session,
853
- status: await inspectTmuxSession(normalized, 40),
854
- session_state: await readSessionState(namespaceDir, listedSessionId as string),
855
- };
856
- }),
1153
+ sessions.map(async session =>
1154
+ typeof session === "object" && session !== null
1155
+ ? {
1156
+ session,
1157
+ status: await inspectTmuxSession(session as Record<string, unknown>, 40, commandRunner),
1158
+ }
1159
+ : { session, status: { live: null } },
1160
+ ),
857
1161
  );
858
1162
  return { ok: true, sessions, statuses };
859
1163
  }
860
1164
  if (name === "gjc_coordinator_read_tail") {
861
- const session = await readJsonFile(sessionFile(args.session_id));
1165
+ const session = asRecord(await readJsonFile(sessionFile(args.session_id)));
862
1166
  return { ok: true, lines: session ? await captureTmuxTail(session, boundedLineCount(args.lines)) : [] };
863
1167
  }
864
- if (name === "gjc_coordinator_list_questions") {
865
- const questions = await listJsonFiles(path.join(namespaceDir, "questions"));
866
- const sessionId = typeof args.session_id === "string" ? safeExternalId("session", args.session_id) : null;
867
- if (sessionId) {
868
- const openQuestion = questions.find(
869
- question =>
870
- question &&
871
- typeof question === "object" &&
872
- (question as { session_id?: unknown }).session_id === sessionId &&
873
- (question as { status?: unknown }).status === "open",
874
- ) as { turn_id?: unknown } | undefined;
875
- if (openQuestion) {
876
- await writeSessionState(namespaceDir, sessionId, "needs_user_input", {
877
- currentTurnId: typeof openQuestion.turn_id === "string" ? openQuestion.turn_id : null,
878
- live: null,
879
- reason: "open_question",
880
- });
881
- }
882
- }
883
- return { ok: true, questions };
884
- }
1168
+ if (name === "gjc_coordinator_list_questions") return { ok: true, questions: await listQuestions(args) };
885
1169
  if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
886
1170
  if (name === "gjc_coordinator_read_artifact")
887
1171
  return await readCoordinatorArtifact(config, { path: args.path });
@@ -898,84 +1182,56 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
898
1182
  };
899
1183
  const started = services.startSession
900
1184
  ? await services.startSession(input)
901
- : await startTmuxSession(config, input, namespaceDir);
902
- const session = normalizeSession(
903
- started ?? { sessionId: `gjc-coordinator-${Date.now()}`, cwd, createdAt: new Date().toISOString() },
904
- );
1185
+ : await startTmuxSession(config, input, namespaceDir, commandRunner);
1186
+ const startedRecord = asRecord(started);
1187
+ if (!startedRecord) throw new Error("coordinator_session_command_required");
1188
+ const session = normalizeSession(startedRecord);
905
1189
  await writeJsonFile(sessionFile(session.session_id), session);
906
- const live = hasTmuxIdentity(session) ? await hasTmuxSession(session) : null;
907
- let turn: TurnRecord | null = null;
908
- let sessionState = await writeSessionState(
909
- namespaceDir,
910
- session.session_id as string,
911
- input.prompt ? "running" : "ready_for_input",
912
- { live, reason: null },
913
- );
914
- if (input.prompt) {
915
- const tmuxKeysSent = session.initialPromptTmuxKeysSent === true;
916
- turn = makeTurnRecord(config, session.session_id as string, input.prompt, "active");
917
- const timestamp = new Date().toISOString();
918
- turn.delivery = {
919
- delivered: false,
920
- queued: !tmuxKeysSent,
921
- target:
922
- typeof session.tmux_target === "string"
923
- ? session.tmux_target
924
- : typeof session.tmuxTarget === "string"
925
- ? session.tmuxTarget
926
- : null,
927
- tmux_keys_sent: tmuxKeysSent,
928
- prompt_acknowledged: false,
929
- state: tmuxKeysSent ? "tmux_keys_sent" : "unavailable",
930
- attempts: [
931
- {
932
- delivered: false,
933
- tmux_keys_sent: tmuxKeysSent,
934
- channel: "tmux_keys",
935
- created_at: timestamp,
936
- reason: tmuxKeysSent ? "awaiting_runtime_ack" : "tmux_delivery_unavailable",
937
- },
938
- ],
939
- };
940
- turn.liveness = { checked_at: timestamp, live, reason: live === false ? "tmux_session_missing" : null };
941
- turn.updated_at = timestamp;
942
- await writeTurnRecord(namespaceDir, turn);
943
- await writeActiveTurn(namespaceDir, turn);
944
- await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`), {
945
- session_id: turn.session_id,
1190
+ const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
1191
+ let sessionState = await writeSessionState(namespaceDir, String(session.session_id), "ready_for_input", {
1192
+ live,
1193
+ reason: null,
1194
+ });
1195
+ if (typeof args.prompt === "string" && args.prompt.length > 0) {
1196
+ const turn = await activateTurn(
1197
+ session,
1198
+ makeTurnRecord(config, String(session.session_id), args.prompt, "active"),
1199
+ );
1200
+ sessionState = (await readSessionState(namespaceDir, turn.session_id)) ?? sessionState;
1201
+ const prompt = {
1202
+ session_id: session.session_id,
946
1203
  turn_id: turn.turn_id,
947
- prompt: input.prompt,
1204
+ prompt: args.prompt,
948
1205
  queued: turn.delivery.queued,
949
1206
  delivered: turn.delivery.delivered,
950
1207
  tmux_keys_sent: turn.delivery.tmux_keys_sent ?? false,
951
1208
  prompt_acknowledged: turn.delivery.prompt_acknowledged ?? false,
952
1209
  created_at: turn.created_at,
953
- });
954
- sessionState = await writeSessionState(
955
- namespaceDir,
956
- turn.session_id,
957
- tmuxKeysSent ? "running" : "stale",
958
- {
959
- currentTurnId: turn.turn_id,
960
- live,
961
- reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
962
- },
963
- );
1210
+ };
1211
+ await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`), prompt);
1212
+ return {
1213
+ ok: true,
1214
+ session,
1215
+ session_state: sessionState,
1216
+ turn,
1217
+ turn_id: turn.turn_id,
1218
+ active_turn_id: turn.turn_id,
1219
+ status: turn.status,
1220
+ queued: turn.delivery.queued,
1221
+ delivered: turn.delivery.delivered,
1222
+ delivery: turn.delivery,
1223
+ };
964
1224
  }
965
- return { ok: true, session, session_state: sessionState, ...(turn ? { turn, turn_id: turn.turn_id } : {}) };
1225
+ return { ok: true, session, session_state: sessionState };
966
1226
  }
967
1227
  if (name === "gjc_coordinator_send_prompt") {
968
1228
  requireCoordinatorMutation(config, "sessions", args);
969
1229
  const sessionId = safeExternalId("session", args.session_id);
970
- const session = await readJsonFile(sessionFile(sessionId));
1230
+ const session = asRecord(await readJsonFile(sessionFile(sessionId)));
971
1231
  if (!session) return { ok: false, reason: "unknown_session", session_id: sessionId };
972
1232
  if (typeof args.prompt !== "string" || args.prompt.length === 0)
973
1233
  return { ok: false, reason: "prompt_required" };
974
- let activeTurn = await readActiveTurn(namespaceDir, sessionId);
975
- if (activeTurn && hasTmuxIdentity(session) && (await hasTmuxSession(session)) === false) {
976
- activeTurn = await markTurnFailedForUnavailableSession(namespaceDir, activeTurn, "tmux_session_missing");
977
- activeTurn = null;
978
- }
1234
+ const activeTurn = await readActiveTurn(namespaceDir, sessionId);
979
1235
  if (activeTurn && args.force !== true && args.queue !== true) {
980
1236
  return {
981
1237
  ok: false,
@@ -996,61 +1252,34 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
996
1252
  await clearActiveTurn(namespaceDir, superseded);
997
1253
  }
998
1254
  const shouldQueue = args.queue === true && args.force !== true;
999
- const turn = makeTurnRecord(config, sessionId, args.prompt, shouldQueue ? "queued" : "active");
1000
- if (!shouldQueue) {
1001
- const tmuxKeysSent = await sendTmuxPrompt(session, args.prompt);
1002
- const timestamp = new Date().toISOString();
1003
- const live = hasTmuxIdentity(session) ? await hasTmuxSession(session) : null;
1004
- turn.delivery = {
1005
- delivered: false,
1006
- queued: !tmuxKeysSent,
1007
- target: typeof session.tmux_target === "string" ? session.tmux_target : null,
1008
- tmux_keys_sent: tmuxKeysSent,
1009
- prompt_acknowledged: false,
1010
- state: tmuxKeysSent ? "tmux_keys_sent" : "unavailable",
1011
- attempts: [
1012
- {
1013
- delivered: false,
1014
- tmux_keys_sent: tmuxKeysSent,
1015
- channel: "tmux_keys",
1016
- created_at: timestamp,
1017
- reason: tmuxKeysSent ? "awaiting_runtime_ack" : "tmux_delivery_unavailable",
1018
- },
1019
- ],
1020
- };
1021
- turn.liveness = { checked_at: timestamp, live, reason: live === false ? "tmux_session_missing" : null };
1022
- turn.updated_at = timestamp;
1023
- await writeActiveTurn(namespaceDir, turn);
1024
- await writeSessionState(namespaceDir, sessionId, tmuxKeysSent ? "running" : "stale", {
1025
- currentTurnId: turn.turn_id,
1026
- live,
1027
- reason: tmuxKeysSent ? null : "tmux_delivery_unavailable",
1028
- });
1029
- }
1030
- await writeTurnRecord(namespaceDir, turn);
1031
- const queued = {
1255
+ const turn = shouldQueue
1256
+ ? makeTurnRecord(config, sessionId, args.prompt, "queued")
1257
+ : await activateTurn(session, makeTurnRecord(config, sessionId, args.prompt, "active"));
1258
+ if (shouldQueue) await writeTurnRecord(namespaceDir, turn);
1259
+ const recordedTurn = turn;
1260
+ const prompt = {
1032
1261
  session_id: sessionId,
1033
- turn_id: turn.turn_id,
1262
+ turn_id: recordedTurn.turn_id,
1034
1263
  prompt: args.prompt,
1035
- queued: turn.delivery.queued,
1036
- delivered: turn.delivery.delivered,
1037
- tmux_keys_sent: turn.delivery.tmux_keys_sent ?? false,
1038
- prompt_acknowledged: turn.delivery.prompt_acknowledged ?? false,
1039
- created_at: turn.created_at,
1264
+ queued: recordedTurn.delivery.queued,
1265
+ delivered: recordedTurn.delivery.delivered,
1266
+ tmux_keys_sent: recordedTurn.delivery.tmux_keys_sent ?? false,
1267
+ prompt_acknowledged: recordedTurn.delivery.prompt_acknowledged ?? false,
1268
+ created_at: recordedTurn.created_at,
1040
1269
  };
1041
- await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`), queued);
1270
+ await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`), prompt);
1042
1271
  return {
1043
1272
  ok: true,
1044
1273
  session_id: sessionId,
1045
- turn_id: turn.turn_id,
1046
- active_turn_id: shouldQueue ? activeTurn?.turn_id : turn.turn_id,
1047
- status: turn.status,
1048
- queued: turn.delivery.queued,
1049
- delivered: turn.delivery.delivered,
1050
- delivery: turn.delivery,
1051
- prompt: queued,
1052
- tmux_keys_sent: turn.delivery.tmux_keys_sent ?? false,
1053
- prompt_acknowledged: turn.delivery.prompt_acknowledged ?? false,
1274
+ turn_id: recordedTurn.turn_id,
1275
+ active_turn_id: shouldQueue ? activeTurn?.turn_id : recordedTurn.turn_id,
1276
+ status: recordedTurn.status,
1277
+ queued: recordedTurn.delivery.queued,
1278
+ delivered: recordedTurn.delivery.delivered,
1279
+ delivery: recordedTurn.delivery,
1280
+ prompt,
1281
+ tmux_keys_sent: recordedTurn.delivery.tmux_keys_sent ?? false,
1282
+ prompt_acknowledged: recordedTurn.delivery.prompt_acknowledged ?? false,
1054
1283
  session_state: await readSessionState(namespaceDir, sessionId),
1055
1284
  };
1056
1285
  }
@@ -1090,7 +1319,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1090
1319
  requireCoordinatorMutation(config, "questions", args);
1091
1320
  const questionId = safeExternalId("question", args.question_id);
1092
1321
  const questionPath = questionFile(namespaceDir, questionId);
1093
- const question = await readJsonFile(questionPath);
1322
+ const question = asRecord(await readJsonFile(questionPath));
1094
1323
  if (!question) return { ok: false, reason: "unknown_question" };
1095
1324
  if (args.session_id != null && question.session_id !== safeExternalId("session", args.session_id)) {
1096
1325
  return { ok: false, reason: "question_session_mismatch" };
@@ -1098,6 +1327,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1098
1327
  if (args.turn_id != null && question.turn_id !== safeTurnId(args.turn_id)) {
1099
1328
  return { ok: false, reason: "question_turn_mismatch" };
1100
1329
  }
1330
+ const answeredTurnId = typeof question.turn_id === "string" ? question.turn_id : null;
1101
1331
  const answered = {
1102
1332
  ...question,
1103
1333
  status: "answered",
@@ -1106,8 +1336,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1106
1336
  };
1107
1337
  await writeJsonFile(questionPath, answered);
1108
1338
  let turn: TurnRecord | null = null;
1109
- if (typeof answered.turn_id === "string") {
1110
- turn = await readTurnRecord(namespaceDir, answered.turn_id);
1339
+ if (answeredTurnId) {
1340
+ turn = await readTurnRecord(namespaceDir, answeredTurnId);
1111
1341
  if (turn) {
1112
1342
  const timestamp = new Date().toISOString();
1113
1343
  turn = {
@@ -1123,29 +1353,33 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1123
1353
  live: null,
1124
1354
  reason: null,
1125
1355
  });
1126
- const session = await readJsonFile(sessionFile(turn.session_id));
1127
- if (session && typeof args.answer === "string") await sendTmuxPrompt(session, args.answer);
1356
+ const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
1357
+ if (session && typeof args.answer === "string")
1358
+ await sendTmuxPrompt(session, args.answer, commandRunner);
1128
1359
  }
1129
1360
  }
1130
1361
  return { ok: true, question: answered, ...(turn ? { turn } : {}) };
1131
1362
  }
1132
1363
  if (name === "gjc_coordinator_report_status") {
1133
1364
  requireCoordinatorMutation(config, "reports", args);
1365
+ const evidence = await validateEvidencePaths(args.evidence_paths);
1366
+ const sessionId = args.session_id == null ? null : safeExternalId("session", args.session_id);
1134
1367
  const report = {
1135
- session_id: args.session_id,
1368
+ session_id: sessionId,
1136
1369
  turn_id: args.turn_id,
1137
1370
  status: args.status,
1138
1371
  summary: args.summary,
1139
1372
  blocker: args.blocker,
1140
1373
  pr_url: args.pr_url,
1141
- evidence_paths: args.evidence_paths ?? [],
1374
+ evidence_paths: evidence.map(item => item.path),
1142
1375
  created_at: new Date().toISOString(),
1143
1376
  };
1144
1377
  let turn: TurnRecord | null = null;
1378
+ let promotedTurn: TurnRecord | null = null;
1145
1379
  if (args.turn_id != null) {
1146
1380
  turn = await readTurnRecord(namespaceDir, args.turn_id);
1147
1381
  if (!turn) return { ok: false, reason: "unknown_turn" };
1148
- if (args.session_id != null && turn.session_id !== safeExternalId("session", args.session_id)) {
1382
+ if (sessionId != null && turn.session_id !== sessionId) {
1149
1383
  return { ok: false, reason: "turn_session_mismatch" };
1150
1384
  }
1151
1385
  const terminalStatus = asTerminalTurnStatus(args.status);
@@ -1171,9 +1405,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1171
1405
  artifact_path: null,
1172
1406
  truncated: false,
1173
1407
  },
1174
- evidence: Array.isArray(args.evidence_paths)
1175
- ? args.evidence_paths.map(evidencePath => ({ path: evidencePath }))
1176
- : [],
1408
+ evidence,
1177
1409
  error:
1178
1410
  terminalStatus === "failed"
1179
1411
  ? {
@@ -1198,6 +1430,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1198
1430
  reason: terminalStatus === "failed" ? "reported_failure" : null,
1199
1431
  },
1200
1432
  );
1433
+ promotedTurn = await promoteNextQueuedTurn(turn.session_id);
1201
1434
  }
1202
1435
  }
1203
1436
  await writeJsonFile(path.join(namespaceDir, "reports", `${Date.now()}.json`), report);
@@ -1205,6 +1438,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
1205
1438
  ok: true,
1206
1439
  report,
1207
1440
  ...(turn ? { turn, session_state: await readSessionState(namespaceDir, turn.session_id) } : {}),
1441
+ ...(promotedTurn ? { promoted_turn: promotedTurn } : {}),
1208
1442
  };
1209
1443
  }
1210
1444
  return { ok: false, reason: "unknown_tool", tool: name };
@@ -1254,7 +1488,7 @@ function legacyToolResult(payload: unknown): { content: Array<{ type: "text"; te
1254
1488
  export async function handleCoordinatorMcpRequest(
1255
1489
  request: JsonRpcRequest,
1256
1490
  options: LegacyHandlerOptions = {},
1257
- ): Promise<any> {
1491
+ ): Promise<JsonRpcResponse> {
1258
1492
  if (request.method === "initialize") {
1259
1493
  return {
1260
1494
  jsonrpc: "2.0",