@gajae-code/coding-agent 0.4.4 → 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.
- package/CHANGELOG.md +40 -0
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +2 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/types.d.ts +9 -1
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +8 -0
- package/dist/types/setup/hermes-setup.d.ts +7 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +10 -0
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +12 -3
- package/src/cli.ts +107 -16
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +92 -9
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/setup.ts +4 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +385 -182
- package/src/cursor.ts +30 -2
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +9 -1
- package/src/harness-control-plane/phase-rollup.ts +96 -0
- package/src/harness-control-plane/receipt-ingest.ts +127 -0
- package/src/harness-control-plane/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/types.ts +29 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/main.ts +7 -3
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/abort-message.ts +41 -0
- package/src/modes/utils/context-usage.ts +15 -8
- package/src/modes/utils/ui-helpers.ts +5 -6
- package/src/sdk.ts +9 -4
- package/src/session/agent-session.ts +16 -5
- package/src/session/session-manager.ts +20 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +2 -0
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +7 -0
- package/src/tools/index.ts +2 -2
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/title-generator.ts +16 -2
|
@@ -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?:
|
|
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:
|
|
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<
|
|
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<
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
575
|
-
|
|
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,64 @@ 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
|
|
590
|
-
if (!config.sessionCommand)
|
|
745
|
+
): Promise<Record<string, unknown>> {
|
|
746
|
+
if (!config.sessionCommand) throw new Error("coordinator_session_command_required");
|
|
591
747
|
const sessionName = `gjc-coordinator-${randomUUID().slice(0, 8)}`;
|
|
592
748
|
const runtimeStateFile = sessionStateFile(namespaceDir, sessionName);
|
|
593
749
|
const sessionCommand = [
|
|
@@ -635,16 +791,23 @@ async function captureTmuxTail(session: Record<string, unknown>, lines: number):
|
|
|
635
791
|
return captured.stdout.split("\n").slice(-lines);
|
|
636
792
|
}
|
|
637
793
|
|
|
638
|
-
async function sendTmuxPrompt(
|
|
794
|
+
async function sendTmuxPrompt(
|
|
795
|
+
session: Record<string, unknown>,
|
|
796
|
+
prompt: string,
|
|
797
|
+
runner: CommandRunner = runCommand,
|
|
798
|
+
): Promise<boolean> {
|
|
639
799
|
const target = typeof session.tmux_target === "string" ? session.tmux_target : session.tmuxTarget;
|
|
640
800
|
if (typeof target !== "string" || target.length === 0) return false;
|
|
641
|
-
return await sendTmuxPromptKeys(target, prompt);
|
|
801
|
+
return await sendTmuxPromptKeys(target, prompt, runner);
|
|
642
802
|
}
|
|
643
803
|
|
|
644
|
-
async function hasTmuxSession(
|
|
804
|
+
async function hasTmuxSession(
|
|
805
|
+
session: Record<string, unknown>,
|
|
806
|
+
runner: CommandRunner = runCommand,
|
|
807
|
+
): Promise<boolean | null> {
|
|
645
808
|
const tmuxSession = typeof session.tmux_session === "string" ? session.tmux_session : session.tmuxSession;
|
|
646
809
|
if (typeof tmuxSession !== "string" || tmuxSession.length === 0) return null;
|
|
647
|
-
const checked = await
|
|
810
|
+
const checked = await runner(["tmux", "has-session", "-t", tmuxSession]);
|
|
648
811
|
return checked.exitCode === 0;
|
|
649
812
|
}
|
|
650
813
|
|
|
@@ -673,8 +836,12 @@ function summarizePaneTail(lines: string[]): Record<string, unknown> {
|
|
|
673
836
|
};
|
|
674
837
|
}
|
|
675
838
|
|
|
676
|
-
async function inspectTmuxSession(
|
|
677
|
-
|
|
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);
|
|
678
845
|
const tail = live ? await captureTmuxTail(session, lines) : [];
|
|
679
846
|
return {
|
|
680
847
|
live,
|
|
@@ -763,6 +930,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
763
930
|
const config = buildCoordinatorMcpConfig(options.env ?? process.env);
|
|
764
931
|
const services = options.services ?? {};
|
|
765
932
|
const namespaceDir = coordinatorNamespacePath(config);
|
|
933
|
+
const commandRunner = services.commandRunner ?? runCommand;
|
|
766
934
|
|
|
767
935
|
async function listSessions(): Promise<unknown[]> {
|
|
768
936
|
if (!config.namespace.profile || !config.namespace.repo) return [];
|
|
@@ -772,6 +940,78 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
772
940
|
function sessionFile(sessionId: unknown): string {
|
|
773
941
|
return path.join(namespaceDir, "sessions", `${safeExternalId("session", sessionId)}.json`);
|
|
774
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
|
+
}
|
|
775
1015
|
|
|
776
1016
|
async function readTurnPayload(
|
|
777
1017
|
turnId: unknown,
|
|
@@ -783,7 +1023,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
783
1023
|
if (sessionId != null && turn.session_id !== safeExternalId("session", sessionId)) {
|
|
784
1024
|
return { ok: false, reason: "turn_session_mismatch" };
|
|
785
1025
|
}
|
|
786
|
-
const session = await readJsonFile(sessionFile(turn.session_id));
|
|
1026
|
+
const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
|
|
787
1027
|
let resolvedTurn = turn;
|
|
788
1028
|
let advisoryStatus: Record<string, unknown> = { live: false };
|
|
789
1029
|
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
@@ -811,77 +1051,88 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
811
1051
|
resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "session_record_missing");
|
|
812
1052
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
813
1053
|
} else if (session) {
|
|
814
|
-
advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines));
|
|
1054
|
+
advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines), commandRunner);
|
|
815
1055
|
if (ACTIVE_TURN_STATUSES.has(turn.status) && hasTmuxIdentity(session) && advisoryStatus.live === false) {
|
|
816
1056
|
resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, turn, "tmux_session_missing");
|
|
817
1057
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
818
1058
|
}
|
|
819
1059
|
}
|
|
1060
|
+
const missingFinalResponse =
|
|
1061
|
+
resolvedTurn.status === "completed" && !reportableFinalResponse(resolvedTurn.final_response);
|
|
820
1062
|
return {
|
|
821
1063
|
ok: true,
|
|
822
1064
|
turn: resolvedTurn,
|
|
823
1065
|
advisory_status: advisoryStatus,
|
|
824
1066
|
session_state: sessionState,
|
|
1067
|
+
...(missingFinalResponse
|
|
1068
|
+
? {
|
|
1069
|
+
completion_missing_final_response: true,
|
|
1070
|
+
advisory: MISSING_FINAL_RESPONSE_ADVISORY,
|
|
1071
|
+
}
|
|
1072
|
+
: {}),
|
|
825
1073
|
};
|
|
826
1074
|
}
|
|
827
1075
|
|
|
828
1076
|
async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
829
1077
|
try {
|
|
830
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
|
+
}
|
|
831
1107
|
if (name === "gjc_coordinator_read_status") {
|
|
832
1108
|
const sessionId = args.session_id;
|
|
833
1109
|
if (sessionId) {
|
|
834
|
-
const session = await readJsonFile(sessionFile(sessionId));
|
|
1110
|
+
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
835
1111
|
return {
|
|
836
1112
|
ok: true,
|
|
837
1113
|
session,
|
|
838
|
-
status: session ? await inspectTmuxSession(session) : { live: false },
|
|
1114
|
+
status: session ? await inspectTmuxSession(session, 80, commandRunner) : { live: false },
|
|
839
1115
|
session_state: await readSessionState(namespaceDir, safeExternalId("session", sessionId)),
|
|
840
1116
|
};
|
|
841
1117
|
}
|
|
842
1118
|
const sessions = await listSessions();
|
|
843
1119
|
const statuses = await Promise.all(
|
|
844
|
-
sessions.map(async session =>
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
session,
|
|
853
|
-
status: await inspectTmuxSession(normalized, 40),
|
|
854
|
-
session_state: await readSessionState(namespaceDir, listedSessionId as string),
|
|
855
|
-
};
|
|
856
|
-
}),
|
|
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
|
+
),
|
|
857
1128
|
);
|
|
858
1129
|
return { ok: true, sessions, statuses };
|
|
859
1130
|
}
|
|
860
1131
|
if (name === "gjc_coordinator_read_tail") {
|
|
861
|
-
const session = await readJsonFile(sessionFile(args.session_id));
|
|
1132
|
+
const session = asRecord(await readJsonFile(sessionFile(args.session_id)));
|
|
862
1133
|
return { ok: true, lines: session ? await captureTmuxTail(session, boundedLineCount(args.lines)) : [] };
|
|
863
1134
|
}
|
|
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
|
-
}
|
|
1135
|
+
if (name === "gjc_coordinator_list_questions") return { ok: true, questions: await listQuestions(args) };
|
|
885
1136
|
if (name === "gjc_coordinator_list_artifacts") return { ok: true, roots: config.allowedRoots };
|
|
886
1137
|
if (name === "gjc_coordinator_read_artifact")
|
|
887
1138
|
return await readCoordinatorArtifact(config, { path: args.path });
|
|
@@ -899,83 +1150,57 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
899
1150
|
const started = services.startSession
|
|
900
1151
|
? await services.startSession(input)
|
|
901
1152
|
: await startTmuxSession(config, input, namespaceDir);
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
);
|
|
1153
|
+
const startedRecord = asRecord(started);
|
|
1154
|
+
if (!startedRecord) throw new Error("coordinator_session_command_required");
|
|
1155
|
+
const session = normalizeSession(startedRecord);
|
|
905
1156
|
await writeJsonFile(sessionFile(session.session_id), session);
|
|
906
|
-
const live = hasTmuxIdentity(session) ? await hasTmuxSession(session) : null;
|
|
907
|
-
let turn: TurnRecord | null = null;
|
|
1157
|
+
const live = hasTmuxIdentity(session) ? await hasTmuxSession(session, commandRunner) : null;
|
|
908
1158
|
let sessionState = await writeSessionState(
|
|
909
1159
|
namespaceDir,
|
|
910
|
-
session.session_id
|
|
1160
|
+
String(session.session_id),
|
|
911
1161
|
input.prompt ? "running" : "ready_for_input",
|
|
912
1162
|
{ live, reason: null },
|
|
913
1163
|
);
|
|
914
|
-
if (
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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,
|
|
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,
|
|
946
1172
|
turn_id: turn.turn_id,
|
|
947
|
-
prompt:
|
|
1173
|
+
prompt: args.prompt,
|
|
948
1174
|
queued: turn.delivery.queued,
|
|
949
1175
|
delivered: turn.delivery.delivered,
|
|
950
1176
|
tmux_keys_sent: turn.delivery.tmux_keys_sent ?? false,
|
|
951
1177
|
prompt_acknowledged: turn.delivery.prompt_acknowledged ?? false,
|
|
952
1178
|
created_at: turn.created_at,
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
+
};
|
|
964
1193
|
}
|
|
965
|
-
return { ok: true, session, session_state: sessionState
|
|
1194
|
+
return { ok: true, session, session_state: sessionState };
|
|
966
1195
|
}
|
|
967
1196
|
if (name === "gjc_coordinator_send_prompt") {
|
|
968
1197
|
requireCoordinatorMutation(config, "sessions", args);
|
|
969
1198
|
const sessionId = safeExternalId("session", args.session_id);
|
|
970
|
-
const session = await readJsonFile(sessionFile(sessionId));
|
|
1199
|
+
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
971
1200
|
if (!session) return { ok: false, reason: "unknown_session", session_id: sessionId };
|
|
972
1201
|
if (typeof args.prompt !== "string" || args.prompt.length === 0)
|
|
973
1202
|
return { ok: false, reason: "prompt_required" };
|
|
974
|
-
|
|
975
|
-
if (activeTurn && hasTmuxIdentity(session) && (await hasTmuxSession(session)) === false) {
|
|
976
|
-
activeTurn = await markTurnFailedForUnavailableSession(namespaceDir, activeTurn, "tmux_session_missing");
|
|
977
|
-
activeTurn = null;
|
|
978
|
-
}
|
|
1203
|
+
const activeTurn = await readActiveTurn(namespaceDir, sessionId);
|
|
979
1204
|
if (activeTurn && args.force !== true && args.queue !== true) {
|
|
980
1205
|
return {
|
|
981
1206
|
ok: false,
|
|
@@ -996,61 +1221,34 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
996
1221
|
await clearActiveTurn(namespaceDir, superseded);
|
|
997
1222
|
}
|
|
998
1223
|
const shouldQueue = args.queue === true && args.force !== true;
|
|
999
|
-
const turn =
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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 = {
|
|
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 = {
|
|
1032
1230
|
session_id: sessionId,
|
|
1033
|
-
turn_id:
|
|
1231
|
+
turn_id: recordedTurn.turn_id,
|
|
1034
1232
|
prompt: args.prompt,
|
|
1035
|
-
queued:
|
|
1036
|
-
delivered:
|
|
1037
|
-
tmux_keys_sent:
|
|
1038
|
-
prompt_acknowledged:
|
|
1039
|
-
created_at:
|
|
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,
|
|
1040
1238
|
};
|
|
1041
|
-
await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`),
|
|
1239
|
+
await writeJsonFile(path.join(namespaceDir, "prompts", `${Date.now()}.json`), prompt);
|
|
1042
1240
|
return {
|
|
1043
1241
|
ok: true,
|
|
1044
1242
|
session_id: sessionId,
|
|
1045
|
-
turn_id:
|
|
1046
|
-
active_turn_id: shouldQueue ? activeTurn?.turn_id :
|
|
1047
|
-
status:
|
|
1048
|
-
queued:
|
|
1049
|
-
delivered:
|
|
1050
|
-
delivery:
|
|
1051
|
-
prompt
|
|
1052
|
-
tmux_keys_sent:
|
|
1053
|
-
prompt_acknowledged:
|
|
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,
|
|
1054
1252
|
session_state: await readSessionState(namespaceDir, sessionId),
|
|
1055
1253
|
};
|
|
1056
1254
|
}
|
|
@@ -1090,7 +1288,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1090
1288
|
requireCoordinatorMutation(config, "questions", args);
|
|
1091
1289
|
const questionId = safeExternalId("question", args.question_id);
|
|
1092
1290
|
const questionPath = questionFile(namespaceDir, questionId);
|
|
1093
|
-
const question = await readJsonFile(questionPath);
|
|
1291
|
+
const question = asRecord(await readJsonFile(questionPath));
|
|
1094
1292
|
if (!question) return { ok: false, reason: "unknown_question" };
|
|
1095
1293
|
if (args.session_id != null && question.session_id !== safeExternalId("session", args.session_id)) {
|
|
1096
1294
|
return { ok: false, reason: "question_session_mismatch" };
|
|
@@ -1098,6 +1296,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1098
1296
|
if (args.turn_id != null && question.turn_id !== safeTurnId(args.turn_id)) {
|
|
1099
1297
|
return { ok: false, reason: "question_turn_mismatch" };
|
|
1100
1298
|
}
|
|
1299
|
+
const answeredTurnId = typeof question.turn_id === "string" ? question.turn_id : null;
|
|
1101
1300
|
const answered = {
|
|
1102
1301
|
...question,
|
|
1103
1302
|
status: "answered",
|
|
@@ -1106,8 +1305,8 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1106
1305
|
};
|
|
1107
1306
|
await writeJsonFile(questionPath, answered);
|
|
1108
1307
|
let turn: TurnRecord | null = null;
|
|
1109
|
-
if (
|
|
1110
|
-
turn = await readTurnRecord(namespaceDir,
|
|
1308
|
+
if (answeredTurnId) {
|
|
1309
|
+
turn = await readTurnRecord(namespaceDir, answeredTurnId);
|
|
1111
1310
|
if (turn) {
|
|
1112
1311
|
const timestamp = new Date().toISOString();
|
|
1113
1312
|
turn = {
|
|
@@ -1123,29 +1322,33 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1123
1322
|
live: null,
|
|
1124
1323
|
reason: null,
|
|
1125
1324
|
});
|
|
1126
|
-
const session = await readJsonFile(sessionFile(turn.session_id));
|
|
1127
|
-
if (session && typeof args.answer === "string")
|
|
1325
|
+
const session = asRecord(await readJsonFile(sessionFile(turn.session_id)));
|
|
1326
|
+
if (session && typeof args.answer === "string")
|
|
1327
|
+
await sendTmuxPrompt(session, args.answer, commandRunner);
|
|
1128
1328
|
}
|
|
1129
1329
|
}
|
|
1130
1330
|
return { ok: true, question: answered, ...(turn ? { turn } : {}) };
|
|
1131
1331
|
}
|
|
1132
1332
|
if (name === "gjc_coordinator_report_status") {
|
|
1133
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);
|
|
1134
1336
|
const report = {
|
|
1135
|
-
session_id:
|
|
1337
|
+
session_id: sessionId,
|
|
1136
1338
|
turn_id: args.turn_id,
|
|
1137
1339
|
status: args.status,
|
|
1138
1340
|
summary: args.summary,
|
|
1139
1341
|
blocker: args.blocker,
|
|
1140
1342
|
pr_url: args.pr_url,
|
|
1141
|
-
evidence_paths:
|
|
1343
|
+
evidence_paths: evidence.map(item => item.path),
|
|
1142
1344
|
created_at: new Date().toISOString(),
|
|
1143
1345
|
};
|
|
1144
1346
|
let turn: TurnRecord | null = null;
|
|
1347
|
+
let promotedTurn: TurnRecord | null = null;
|
|
1145
1348
|
if (args.turn_id != null) {
|
|
1146
1349
|
turn = await readTurnRecord(namespaceDir, args.turn_id);
|
|
1147
1350
|
if (!turn) return { ok: false, reason: "unknown_turn" };
|
|
1148
|
-
if (
|
|
1351
|
+
if (sessionId != null && turn.session_id !== sessionId) {
|
|
1149
1352
|
return { ok: false, reason: "turn_session_mismatch" };
|
|
1150
1353
|
}
|
|
1151
1354
|
const terminalStatus = asTerminalTurnStatus(args.status);
|
|
@@ -1171,9 +1374,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1171
1374
|
artifact_path: null,
|
|
1172
1375
|
truncated: false,
|
|
1173
1376
|
},
|
|
1174
|
-
evidence
|
|
1175
|
-
? args.evidence_paths.map(evidencePath => ({ path: evidencePath }))
|
|
1176
|
-
: [],
|
|
1377
|
+
evidence,
|
|
1177
1378
|
error:
|
|
1178
1379
|
terminalStatus === "failed"
|
|
1179
1380
|
? {
|
|
@@ -1198,6 +1399,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1198
1399
|
reason: terminalStatus === "failed" ? "reported_failure" : null,
|
|
1199
1400
|
},
|
|
1200
1401
|
);
|
|
1402
|
+
promotedTurn = await promoteNextQueuedTurn(turn.session_id);
|
|
1201
1403
|
}
|
|
1202
1404
|
}
|
|
1203
1405
|
await writeJsonFile(path.join(namespaceDir, "reports", `${Date.now()}.json`), report);
|
|
@@ -1205,6 +1407,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1205
1407
|
ok: true,
|
|
1206
1408
|
report,
|
|
1207
1409
|
...(turn ? { turn, session_state: await readSessionState(namespaceDir, turn.session_id) } : {}),
|
|
1410
|
+
...(promotedTurn ? { promoted_turn: promotedTurn } : {}),
|
|
1208
1411
|
};
|
|
1209
1412
|
}
|
|
1210
1413
|
return { ok: false, reason: "unknown_tool", tool: name };
|
|
@@ -1254,7 +1457,7 @@ function legacyToolResult(payload: unknown): { content: Array<{ type: "text"; te
|
|
|
1254
1457
|
export async function handleCoordinatorMcpRequest(
|
|
1255
1458
|
request: JsonRpcRequest,
|
|
1256
1459
|
options: LegacyHandlerOptions = {},
|
|
1257
|
-
): Promise<
|
|
1460
|
+
): Promise<JsonRpcResponse> {
|
|
1258
1461
|
if (request.method === "initialize") {
|
|
1259
1462
|
return {
|
|
1260
1463
|
jsonrpc: "2.0",
|