@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.
- package/CHANGELOG.md +83 -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 +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- 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/receipt-spool.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/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +13 -1
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +32 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -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 +2 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +17 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- 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 +112 -17
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +128 -11
- package/src/commands/launch.ts +2 -2
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/session.ts +3 -1
- package/src/commands/setup.ts +4 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +427 -193
- package/src/cursor.ts +46 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +87 -28
- 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/receipt-spool.ts +128 -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/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +33 -1
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +7 -3
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- 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 +38 -6
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +121 -25
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +328 -57
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/executor.ts +69 -6
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +7 -0
- package/src/task/render.ts +21 -1
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +15 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +4 -2
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/title-generator.ts +16 -2
- 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?:
|
|
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,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
|
-
|
|
590
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
677
|
-
|
|
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
|
-
|
|
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
|
-
}),
|
|
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
|
|
903
|
-
|
|
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
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
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:
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 = {
|
|
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:
|
|
1262
|
+
turn_id: recordedTurn.turn_id,
|
|
1034
1263
|
prompt: args.prompt,
|
|
1035
|
-
queued:
|
|
1036
|
-
delivered:
|
|
1037
|
-
tmux_keys_sent:
|
|
1038
|
-
prompt_acknowledged:
|
|
1039
|
-
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`),
|
|
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:
|
|
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:
|
|
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 (
|
|
1110
|
-
turn = await readTurnRecord(namespaceDir,
|
|
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")
|
|
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:
|
|
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:
|
|
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 (
|
|
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
|
|
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<
|
|
1491
|
+
): Promise<JsonRpcResponse> {
|
|
1258
1492
|
if (request.method === "initialize") {
|
|
1259
1493
|
return {
|
|
1260
1494
|
jsonrpc: "2.0",
|