@gajae-code/coding-agent 0.7.7 → 0.7.8
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 +20 -1
- package/README.md +8 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +45 -0
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
- package/package.json +7 -7
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/update-cli.ts +155 -13
- package/src/cli.ts +18 -0
- package/src/commands/launch.ts +46 -1
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +47 -0
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/gjc-runtime/launch-tmux.ts +34 -8
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/team-runtime.ts +21 -6
- package/src/gjc-runtime/tmux-common.ts +16 -0
- package/src/gjc-runtime/tmux-sessions.ts +13 -9
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/model-selector.ts +182 -5
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +491 -5
- package/src/modes/components/status-line/segments.ts +7 -18
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +69 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +12 -4
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/telegram-daemon-cli.ts +112 -9
- package/src/notifications/telegram-daemon.ts +4 -0
- package/src/session/agent-session.ts +74 -5
- package/src/slash-commands/builtin-registry.ts +106 -13
|
@@ -119,7 +119,7 @@ interface TurnRecord {
|
|
|
119
119
|
target: string | null;
|
|
120
120
|
tmux_keys_sent?: boolean;
|
|
121
121
|
prompt_acknowledged?: boolean;
|
|
122
|
-
state?: "queued" | "tmux_keys_sent" | "acknowledged" | "unavailable";
|
|
122
|
+
state?: "queued" | "tmux_keys_sent" | "acknowledged" | "unavailable" | "unacknowledged";
|
|
123
123
|
attempts: Array<{
|
|
124
124
|
delivered: boolean;
|
|
125
125
|
created_at: string;
|
|
@@ -214,6 +214,9 @@ interface CoordinatorEventInput {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
|
|
217
|
+
const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
|
|
218
|
+
const DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 10_000;
|
|
219
|
+
const MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
217
220
|
const ACTIVE_TURN_STATUSES = new Set<TurnStatus>(["delivering", "active", "waiting_for_answer", "completing"]);
|
|
218
221
|
const TERMINAL_TURN_STATUSES = new Set<TurnStatus>(["completed", "failed", "cancelled", "superseded"]);
|
|
219
222
|
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;
|
|
@@ -317,8 +320,14 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
317
320
|
properties: {
|
|
318
321
|
session_id: sessionId,
|
|
319
322
|
turn_id: { type: "string" },
|
|
320
|
-
timeout_ms: {
|
|
321
|
-
|
|
323
|
+
timeout_ms: {
|
|
324
|
+
type: "number",
|
|
325
|
+
description: "Bounded await timeout in milliseconds, capped at 30 minutes.",
|
|
326
|
+
},
|
|
327
|
+
poll_interval_ms: {
|
|
328
|
+
type: "number",
|
|
329
|
+
description: "Bounded polling interval in milliseconds, capped at 10 seconds.",
|
|
330
|
+
},
|
|
322
331
|
lines: { type: "number" },
|
|
323
332
|
},
|
|
324
333
|
required: ["turn_id"],
|
|
@@ -406,7 +415,10 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
406
415
|
after_seq: { type: "number" },
|
|
407
416
|
session_id: sessionId,
|
|
408
417
|
event_types: { type: "array", items: { type: "string" } },
|
|
409
|
-
timeout_ms: {
|
|
418
|
+
timeout_ms: {
|
|
419
|
+
type: "number",
|
|
420
|
+
description: "Bounded event long-poll timeout in milliseconds, capped at 30 seconds.",
|
|
421
|
+
},
|
|
410
422
|
limit: { type: "number" },
|
|
411
423
|
},
|
|
412
424
|
},
|
|
@@ -447,7 +459,8 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
447
459
|
await_completion: { type: "boolean", description: "If true, poll the turn until terminal or timeout." },
|
|
448
460
|
timeout_ms: {
|
|
449
461
|
type: "number",
|
|
450
|
-
description:
|
|
462
|
+
description:
|
|
463
|
+
"Bounded await timeout in milliseconds, capped at 30 minutes like gjc_coordinator_await_turn.",
|
|
451
464
|
},
|
|
452
465
|
poll_interval_ms: { type: "number", description: "Bounded await polling interval." },
|
|
453
466
|
lines: { type: "number", description: "Bounded advisory tail lines returned with await/read payloads." },
|
|
@@ -1039,6 +1052,135 @@ async function markTurnTerminalFromSessionState(
|
|
|
1039
1052
|
return resolved;
|
|
1040
1053
|
}
|
|
1041
1054
|
|
|
1055
|
+
function runtimeStateAcknowledgesTurn(turn: TurnRecord, sessionState: CoordinatorSessionState | null): boolean {
|
|
1056
|
+
return (
|
|
1057
|
+
sessionState?.source === "agent_session_event" &&
|
|
1058
|
+
sessionState.current_turn_id === turn.turn_id &&
|
|
1059
|
+
(sessionState.state === "running" ||
|
|
1060
|
+
sessionState.state === "needs_user_input" ||
|
|
1061
|
+
sessionState.state === "completed" ||
|
|
1062
|
+
sessionState.state === "errored")
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function markTurnAcknowledgedFromRuntimeState(
|
|
1067
|
+
namespaceDir: string,
|
|
1068
|
+
turn: TurnRecord,
|
|
1069
|
+
sessionState: CoordinatorSessionState,
|
|
1070
|
+
): Promise<TurnRecord> {
|
|
1071
|
+
if (turn.delivery.prompt_acknowledged === true && turn.delivery.state === "acknowledged") return turn;
|
|
1072
|
+
const timestamp = new Date().toISOString();
|
|
1073
|
+
const acknowledged: TurnRecord = {
|
|
1074
|
+
...turn,
|
|
1075
|
+
delivery: {
|
|
1076
|
+
...turn.delivery,
|
|
1077
|
+
delivered: true,
|
|
1078
|
+
prompt_acknowledged: true,
|
|
1079
|
+
state: "acknowledged",
|
|
1080
|
+
attempts: [
|
|
1081
|
+
...turn.delivery.attempts,
|
|
1082
|
+
{
|
|
1083
|
+
delivered: true,
|
|
1084
|
+
created_at: sessionState.updated_at,
|
|
1085
|
+
reason: "runtime_prompt_acknowledged",
|
|
1086
|
+
channel: "runtime_ack",
|
|
1087
|
+
tmux_keys_sent: turn.delivery.tmux_keys_sent,
|
|
1088
|
+
},
|
|
1089
|
+
],
|
|
1090
|
+
},
|
|
1091
|
+
updated_at: timestamp,
|
|
1092
|
+
};
|
|
1093
|
+
await writeTurnRecord(namespaceDir, acknowledged);
|
|
1094
|
+
await writeActiveTurn(namespaceDir, acknowledged);
|
|
1095
|
+
return acknowledged;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function turnAwaitingRuntimeAckExpired(turn: TurnRecord, nowMs: number, ackTimeoutMs: number): boolean {
|
|
1099
|
+
if (!ACTIVE_TURN_STATUSES.has(turn.status)) return false;
|
|
1100
|
+
if (turn.delivery.tmux_keys_sent !== true) return false;
|
|
1101
|
+
if (turn.delivery.prompt_acknowledged === true) return false;
|
|
1102
|
+
if (turn.delivery.state !== "tmux_keys_sent") return false;
|
|
1103
|
+
const deliveredAt =
|
|
1104
|
+
turn.delivery.attempts.findLast(attempt => attempt.channel === "tmux_keys")?.created_at ?? turn.updated_at;
|
|
1105
|
+
const deliveredMs = Date.parse(deliveredAt);
|
|
1106
|
+
return Number.isFinite(deliveredMs) && nowMs - deliveredMs >= ackTimeoutMs;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function markTurnFailedForUnacknowledgedDelivery(
|
|
1110
|
+
namespaceDir: string,
|
|
1111
|
+
turn: TurnRecord,
|
|
1112
|
+
ackTimeoutMs: number,
|
|
1113
|
+
): Promise<TurnRecord> {
|
|
1114
|
+
const timestamp = new Date().toISOString();
|
|
1115
|
+
const message = `Tmux key delivery succeeded, but the GJC runtime did not acknowledge the prompt or emit turn_start within ${ackTimeoutMs}ms. The turn never started; stop waiting and inspect/retry the coordinator session.`;
|
|
1116
|
+
const failed: TurnRecord = {
|
|
1117
|
+
...turn,
|
|
1118
|
+
status: "failed",
|
|
1119
|
+
delivery: {
|
|
1120
|
+
...turn.delivery,
|
|
1121
|
+
delivered: false,
|
|
1122
|
+
queued: false,
|
|
1123
|
+
prompt_acknowledged: false,
|
|
1124
|
+
state: "unacknowledged",
|
|
1125
|
+
attempts: [
|
|
1126
|
+
...turn.delivery.attempts,
|
|
1127
|
+
{
|
|
1128
|
+
delivered: false,
|
|
1129
|
+
created_at: timestamp,
|
|
1130
|
+
reason: PROMPT_ACK_TIMEOUT_REASON,
|
|
1131
|
+
channel: "runtime_ack",
|
|
1132
|
+
tmux_keys_sent: true,
|
|
1133
|
+
},
|
|
1134
|
+
],
|
|
1135
|
+
},
|
|
1136
|
+
final_response: {
|
|
1137
|
+
text: message,
|
|
1138
|
+
format: "markdown",
|
|
1139
|
+
source: "coordinator_delivery_ack_timeout",
|
|
1140
|
+
artifact_path: null,
|
|
1141
|
+
truncated: false,
|
|
1142
|
+
},
|
|
1143
|
+
error: { code: PROMPT_ACK_TIMEOUT_REASON, message, recoverable: true },
|
|
1144
|
+
evidence: [
|
|
1145
|
+
...turn.evidence,
|
|
1146
|
+
{
|
|
1147
|
+
type: PROMPT_ACK_TIMEOUT_REASON,
|
|
1148
|
+
message,
|
|
1149
|
+
tmux_keys_sent: true,
|
|
1150
|
+
prompt_acknowledged: false,
|
|
1151
|
+
created_at: timestamp,
|
|
1152
|
+
},
|
|
1153
|
+
],
|
|
1154
|
+
liveness: { checked_at: timestamp, live: turn.liveness.live, reason: PROMPT_ACK_TIMEOUT_REASON },
|
|
1155
|
+
updated_at: timestamp,
|
|
1156
|
+
completed_at: timestamp,
|
|
1157
|
+
};
|
|
1158
|
+
await writeTurnRecord(namespaceDir, failed);
|
|
1159
|
+
await clearActiveTurn(namespaceDir, failed);
|
|
1160
|
+
await writeSessionState(namespaceDir, failed.session_id, "stale", {
|
|
1161
|
+
lastTurnId: failed.turn_id,
|
|
1162
|
+
live: failed.liveness.live,
|
|
1163
|
+
reason: PROMPT_ACK_TIMEOUT_REASON,
|
|
1164
|
+
});
|
|
1165
|
+
return failed;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function reconcileRuntimeAcknowledgement(
|
|
1169
|
+
namespaceDir: string,
|
|
1170
|
+
turn: TurnRecord,
|
|
1171
|
+
sessionState: CoordinatorSessionState | null,
|
|
1172
|
+
ackTimeoutMs: number,
|
|
1173
|
+
options: { failOnTimeout: boolean } = { failOnTimeout: true },
|
|
1174
|
+
): Promise<TurnRecord> {
|
|
1175
|
+
if (sessionState && runtimeStateAcknowledgesTurn(turn, sessionState)) {
|
|
1176
|
+
return await markTurnAcknowledgedFromRuntimeState(namespaceDir, turn, sessionState);
|
|
1177
|
+
}
|
|
1178
|
+
if (options.failOnTimeout && turnAwaitingRuntimeAckExpired(turn, Date.now(), ackTimeoutMs)) {
|
|
1179
|
+
return await markTurnFailedForUnacknowledgedDelivery(namespaceDir, turn, ackTimeoutMs);
|
|
1180
|
+
}
|
|
1181
|
+
return turn;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1042
1184
|
function shellQuote(value: string): string {
|
|
1043
1185
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1044
1186
|
}
|
|
@@ -1086,16 +1228,33 @@ function asTerminalTurnStatus(status: unknown): TurnStatus | null {
|
|
|
1086
1228
|
return null;
|
|
1087
1229
|
}
|
|
1088
1230
|
|
|
1089
|
-
|
|
1231
|
+
export const COORDINATOR_AWAIT_TURN_TIMEOUT_MAX_MS = 30 * 60 * 1000;
|
|
1232
|
+
export const COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS = MAX_RUNTIME_PROMPT_ACK_TIMEOUT_MS;
|
|
1233
|
+
export const COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS = 30_000;
|
|
1234
|
+
export const COORDINATOR_POLL_INTERVAL_MAX_MS = 10_000;
|
|
1235
|
+
|
|
1236
|
+
function parsePositiveIntegerMs(value: unknown, fallback: number): number {
|
|
1090
1237
|
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
1091
|
-
|
|
1092
|
-
return Math.min(parsed, 30_000);
|
|
1238
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1093
1239
|
}
|
|
1094
1240
|
|
|
1095
|
-
function
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1241
|
+
export function boundedAwaitTurnTimeoutMs(value: unknown): number {
|
|
1242
|
+
return Math.min(parsePositiveIntegerMs(value, 1000), COORDINATOR_AWAIT_TURN_TIMEOUT_MAX_MS);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export function boundedRuntimePromptAckTimeoutMs(value: unknown): number {
|
|
1246
|
+
return Math.min(
|
|
1247
|
+
parsePositiveIntegerMs(value, DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS),
|
|
1248
|
+
COORDINATOR_RUNTIME_PROMPT_ACK_TIMEOUT_MAX_MS,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
export function boundedEventWatchTimeoutMs(value: unknown): number {
|
|
1253
|
+
return Math.min(parsePositiveIntegerMs(value, 1000), COORDINATOR_EVENT_WATCH_TIMEOUT_MAX_MS);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export function boundedPollIntervalMs(value: unknown): number {
|
|
1257
|
+
return Math.min(Math.max(parsePositiveIntegerMs(value, 100), 10), COORDINATOR_POLL_INTERVAL_MAX_MS);
|
|
1099
1258
|
}
|
|
1100
1259
|
async function runCommand(command: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
1101
1260
|
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
|
|
@@ -1387,7 +1546,9 @@ export async function readCoordinatorArtifact(
|
|
|
1387
1546
|
}
|
|
1388
1547
|
|
|
1389
1548
|
export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions = {}) {
|
|
1390
|
-
const
|
|
1549
|
+
const env = options.env ?? process.env;
|
|
1550
|
+
const config = buildCoordinatorMcpConfig(env);
|
|
1551
|
+
const promptAckTimeoutMs = boundedRuntimePromptAckTimeoutMs(env.GJC_COORDINATOR_MCP_PROMPT_ACK_TIMEOUT_MS);
|
|
1391
1552
|
const services = options.services ?? {};
|
|
1392
1553
|
const namespaceDir = coordinatorNamespacePath(config);
|
|
1393
1554
|
const commandRunner = services.commandRunner ?? runCommand;
|
|
@@ -1485,14 +1646,17 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1485
1646
|
await writeTurnRecord(namespaceDir, activeTurn);
|
|
1486
1647
|
await writeActiveTurn(namespaceDir, activeTurn);
|
|
1487
1648
|
const sessionState = await readSessionState(namespaceDir, activeTurn.session_id);
|
|
1488
|
-
const
|
|
1489
|
-
sessionState
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1649
|
+
const runtimeStateAlreadyAcknowledged =
|
|
1650
|
+
sessionState !== null && runtimeStateAcknowledgesTurn(activeTurn, sessionState);
|
|
1651
|
+
const resolvedTurn =
|
|
1652
|
+
runtimeStateAlreadyAcknowledged && sessionState
|
|
1653
|
+
? await markTurnAcknowledgedFromRuntimeState(namespaceDir, activeTurn, sessionState)
|
|
1654
|
+
: activeTurn;
|
|
1655
|
+
if (!runtimeStateAlreadyAcknowledged && !tmuxKeysSent) {
|
|
1656
|
+
await writeSessionState(namespaceDir, activeTurn.session_id, "stale", {
|
|
1493
1657
|
currentTurnId: activeTurn.turn_id,
|
|
1494
1658
|
live,
|
|
1495
|
-
reason:
|
|
1659
|
+
reason: "tmux_delivery_unavailable",
|
|
1496
1660
|
});
|
|
1497
1661
|
}
|
|
1498
1662
|
await appendCoordinatorEvent(namespaceDir, {
|
|
@@ -1505,7 +1669,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1505
1669
|
payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, activeTurn.turn_id)),
|
|
1506
1670
|
metadata: { target: typeof target === "string" ? target : null, live },
|
|
1507
1671
|
});
|
|
1508
|
-
return
|
|
1672
|
+
return resolvedTurn;
|
|
1509
1673
|
}
|
|
1510
1674
|
|
|
1511
1675
|
async function promoteNextQueuedTurn(sessionId: string): Promise<TurnRecord | null> {
|
|
@@ -1533,33 +1697,67 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1533
1697
|
let resolvedTurn = turn;
|
|
1534
1698
|
let advisoryStatus: Record<string, unknown> = { live: false };
|
|
1535
1699
|
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
1700
|
+
resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
1701
|
+
namespaceDir,
|
|
1702
|
+
resolvedTurn,
|
|
1703
|
+
sessionState,
|
|
1704
|
+
promptAckTimeoutMs,
|
|
1705
|
+
{ failOnTimeout: false },
|
|
1706
|
+
);
|
|
1707
|
+
if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1536
1708
|
if (
|
|
1537
1709
|
sessionState &&
|
|
1538
|
-
ACTIVE_TURN_STATUSES.has(
|
|
1539
|
-
sessionState.current_turn_id ===
|
|
1710
|
+
ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
|
|
1711
|
+
(sessionState.current_turn_id === resolvedTurn.turn_id ||
|
|
1712
|
+
(sessionState.state === "errored" &&
|
|
1713
|
+
sessionState.source === "agent_session_event" &&
|
|
1714
|
+
sessionState.current_turn_id == null)) &&
|
|
1540
1715
|
(sessionState.state === "completed" || sessionState.state === "errored")
|
|
1541
1716
|
) {
|
|
1542
|
-
resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir,
|
|
1717
|
+
resolvedTurn = await markTurnTerminalFromSessionState(namespaceDir, resolvedTurn, sessionState);
|
|
1543
1718
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1544
1719
|
} else if (
|
|
1545
1720
|
sessionState &&
|
|
1546
|
-
ACTIVE_TURN_STATUSES.has(
|
|
1547
|
-
sessionState.current_turn_id ===
|
|
1721
|
+
ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
|
|
1722
|
+
sessionState.current_turn_id === resolvedTurn.turn_id &&
|
|
1548
1723
|
sessionState.state === "stale" &&
|
|
1549
1724
|
sessionState.reason === "tmux_delivery_unavailable" &&
|
|
1550
|
-
|
|
1725
|
+
resolvedTurn.delivery.state === "unavailable" &&
|
|
1551
1726
|
session &&
|
|
1552
1727
|
hasTmuxIdentity(session)
|
|
1553
1728
|
) {
|
|
1554
|
-
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
1729
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
1730
|
+
namespaceDir,
|
|
1731
|
+
resolvedTurn,
|
|
1732
|
+
"tmux_delivery_unavailable",
|
|
1733
|
+
);
|
|
1555
1734
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1556
|
-
} else if (!session && ACTIVE_TURN_STATUSES.has(
|
|
1557
|
-
resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir,
|
|
1735
|
+
} else if (!session && ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1736
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
|
|
1558
1737
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1559
1738
|
} else if (session) {
|
|
1560
1739
|
advisoryStatus = await inspectTmuxSession(session, boundedLineCount(lines), commandRunner);
|
|
1561
|
-
if (
|
|
1562
|
-
resolvedTurn
|
|
1740
|
+
if (
|
|
1741
|
+
ACTIVE_TURN_STATUSES.has(resolvedTurn.status) &&
|
|
1742
|
+
hasTmuxIdentity(session) &&
|
|
1743
|
+
advisoryStatus.live === false
|
|
1744
|
+
) {
|
|
1745
|
+
resolvedTurn = await markTurnFailedForUnavailableSession(
|
|
1746
|
+
namespaceDir,
|
|
1747
|
+
resolvedTurn,
|
|
1748
|
+
"tmux_session_missing",
|
|
1749
|
+
);
|
|
1750
|
+
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
if (ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1754
|
+
resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
1755
|
+
namespaceDir,
|
|
1756
|
+
resolvedTurn,
|
|
1757
|
+
sessionState,
|
|
1758
|
+
promptAckTimeoutMs,
|
|
1759
|
+
);
|
|
1760
|
+
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) {
|
|
1563
1761
|
sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1564
1762
|
}
|
|
1565
1763
|
}
|
|
@@ -1579,6 +1777,46 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1579
1777
|
};
|
|
1580
1778
|
}
|
|
1581
1779
|
|
|
1780
|
+
async function reconcileActiveTurnAcknowledgements(): Promise<void> {
|
|
1781
|
+
const turns = (await listJsonFiles(turnsDir(namespaceDir)))
|
|
1782
|
+
.map(turn => asRecord(turn) as TurnRecord | null)
|
|
1783
|
+
.filter((turn): turn is TurnRecord => turn !== null && ACTIVE_TURN_STATUSES.has(turn.status));
|
|
1784
|
+
for (const turn of turns) {
|
|
1785
|
+
let sessionState = await readSessionState(namespaceDir, turn.session_id);
|
|
1786
|
+
const resolvedTurn = await reconcileRuntimeAcknowledgement(
|
|
1787
|
+
namespaceDir,
|
|
1788
|
+
turn,
|
|
1789
|
+
sessionState,
|
|
1790
|
+
promptAckTimeoutMs,
|
|
1791
|
+
{ failOnTimeout: false },
|
|
1792
|
+
);
|
|
1793
|
+
if (!ACTIVE_TURN_STATUSES.has(resolvedTurn.status)) continue;
|
|
1794
|
+
if (resolvedTurn !== turn) sessionState = await readSessionState(namespaceDir, resolvedTurn.session_id);
|
|
1795
|
+
const session = asRecord(await readJsonFile(sessionFile(resolvedTurn.session_id)));
|
|
1796
|
+
if (
|
|
1797
|
+
sessionState &&
|
|
1798
|
+
sessionState.current_turn_id === resolvedTurn.turn_id &&
|
|
1799
|
+
sessionState.state === "stale" &&
|
|
1800
|
+
sessionState.reason === "tmux_delivery_unavailable" &&
|
|
1801
|
+
resolvedTurn.delivery.state === "unavailable" &&
|
|
1802
|
+
session &&
|
|
1803
|
+
hasTmuxIdentity(session)
|
|
1804
|
+
) {
|
|
1805
|
+
await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "tmux_delivery_unavailable");
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
if (!session) {
|
|
1809
|
+
await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "session_record_missing");
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
if (hasTmuxIdentity(session) && (await hasTmuxSession(session, commandRunner)) === false) {
|
|
1813
|
+
await markTurnFailedForUnavailableSession(namespaceDir, resolvedTurn, "tmux_session_missing");
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
await reconcileRuntimeAcknowledgement(namespaceDir, resolvedTurn, sessionState, promptAckTimeoutMs);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1582
1820
|
async function callTool(name: string, args: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
1583
1821
|
try {
|
|
1584
1822
|
if (name === "gjc_coordinator_list_sessions") return { ok: true, sessions: await listSessions() };
|
|
@@ -1619,6 +1857,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1619
1857
|
};
|
|
1620
1858
|
}
|
|
1621
1859
|
if (name === "gjc_coordinator_read_status") {
|
|
1860
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1622
1861
|
const sessionId = args.session_id;
|
|
1623
1862
|
if (sessionId) {
|
|
1624
1863
|
const session = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
@@ -1651,6 +1890,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1651
1890
|
if (name === "gjc_coordinator_read_artifact")
|
|
1652
1891
|
return await readCoordinatorArtifact(config, { path: args.path });
|
|
1653
1892
|
if (name === "gjc_coordinator_read_coordination_status") {
|
|
1893
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1654
1894
|
const sessions = jsonRecords(await listSessions());
|
|
1655
1895
|
const sessionStates = jsonRecords(await listJsonFiles(path.join(namespaceDir, "session-states")));
|
|
1656
1896
|
const turns = jsonRecords(await listJsonFiles(turnsDir(namespaceDir)));
|
|
@@ -1684,13 +1924,15 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1684
1924
|
};
|
|
1685
1925
|
}
|
|
1686
1926
|
if (name === "gjc_coordinator_watch_events") {
|
|
1927
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1687
1928
|
const limit = boundedEventLimit(args.limit);
|
|
1688
|
-
const timeoutMs =
|
|
1929
|
+
const timeoutMs = boundedEventWatchTimeoutMs(args.timeout_ms);
|
|
1689
1930
|
let events = await readCoordinatorEvents(namespaceDir);
|
|
1690
1931
|
let matched = filterCoordinatorEvents(events, args, limit);
|
|
1691
1932
|
let timedOut = false;
|
|
1692
1933
|
if (matched.length === 0 && timeoutMs > 0) {
|
|
1693
1934
|
await waitForCoordinatorEvents(namespaceDir, timeoutMs);
|
|
1935
|
+
await reconcileActiveTurnAcknowledgements();
|
|
1694
1936
|
events = await readCoordinatorEvents(namespaceDir);
|
|
1695
1937
|
matched = filterCoordinatorEvents(events, args, limit);
|
|
1696
1938
|
timedOut = matched.length === 0;
|
|
@@ -1819,7 +2061,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1819
2061
|
};
|
|
1820
2062
|
if (promptAliasIgnored) base.prompt_alias_ignored = true;
|
|
1821
2063
|
if (args.await_completion === true && !shouldQueue) {
|
|
1822
|
-
const timeoutMs =
|
|
2064
|
+
const timeoutMs = boundedAwaitTurnTimeoutMs(args.timeout_ms);
|
|
1823
2065
|
const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
|
|
1824
2066
|
const deadline = Date.now() + timeoutMs;
|
|
1825
2067
|
let payload = await readTurnPayload(turn.turn_id, sessionId, args.lines);
|
|
@@ -1979,7 +2221,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
1979
2221
|
return await readTurnPayload(args.turn_id, args.session_id, args.lines);
|
|
1980
2222
|
}
|
|
1981
2223
|
if (name === "gjc_coordinator_await_turn") {
|
|
1982
|
-
const timeoutMs =
|
|
2224
|
+
const timeoutMs = boundedAwaitTurnTimeoutMs(args.timeout_ms);
|
|
1983
2225
|
const pollIntervalMs = boundedPollIntervalMs(args.poll_interval_ms);
|
|
1984
2226
|
const deadline = Date.now() + timeoutMs;
|
|
1985
2227
|
let payload = await readTurnPayload(args.turn_id, args.session_id, args.lines);
|
|
@@ -6,6 +6,7 @@ import type { Args } from "../cli/args";
|
|
|
6
6
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
7
7
|
import { GJC_COORDINATOR_SESSION_ID_ENV, GJC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
|
|
8
8
|
import {
|
|
9
|
+
buildGjcTmuxExactSessionTarget,
|
|
9
10
|
buildGjcTmuxProfileCommands,
|
|
10
11
|
buildGjcTmuxSessionName,
|
|
11
12
|
buildGjcTmuxSessionSlug,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope, type GjcTmuxSessionStatus } from "./tmux-sessions";
|
|
22
23
|
|
|
23
24
|
export {
|
|
25
|
+
buildGjcTmuxExactSessionTarget,
|
|
24
26
|
buildGjcTmuxProfileCommands,
|
|
25
27
|
GJC_DEFAULT_TMUX_SESSION,
|
|
26
28
|
GJC_TMUX_COMMAND_ENV,
|
|
@@ -201,12 +203,24 @@ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, r
|
|
|
201
203
|
const envLines = Object.entries({ [GJC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
|
|
202
204
|
([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
|
|
203
205
|
);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
// The inner `&` invocation must wrap the resolved gjc command in a
|
|
207
|
+
// script-block so the trailing PowerShell exec-policy flags from the
|
|
208
|
+
// outer invocation are not forwarded into the bun / node / .bat
|
|
209
|
+
// binary the gjc command resolves to. Without the wrapping, the flags
|
|
210
|
+
// reach the runtime binary and cause an immediate failure that closes
|
|
211
|
+
// the psmux pane before the gjc --tmux attach can land.
|
|
212
|
+
const resolvedCommand = command.map(powershellQuote).join(" ");
|
|
213
|
+
const innerArgs = stripRootTmuxFlag(rawArgs).map(powershellQuote).join(" ");
|
|
214
|
+
const invocation = `& { ${resolvedCommand} ${innerArgs} }`;
|
|
207
215
|
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
208
216
|
const script = [...envLines, invocation, exitLine].join("\n");
|
|
209
|
-
|
|
217
|
+
// PowerShell -EncodedCommand requires a UTF-16LE BOM (0xFF 0xFE) at the
|
|
218
|
+
// start of the decoded buffer; without it pwsh may misinterpret the
|
|
219
|
+
// leading bytes and reject the script with a parse error, which would
|
|
220
|
+
// kill the psmux pane before the gjc --tmux attach could land.
|
|
221
|
+
const bom = Buffer.from([0xff, 0xfe]);
|
|
222
|
+
const body = Buffer.from(script, "utf16le");
|
|
223
|
+
const encodedCommand = Buffer.concat([bom, body]).toString("base64");
|
|
210
224
|
return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
|
|
211
225
|
}
|
|
212
226
|
|
|
@@ -395,7 +409,11 @@ function readCurrentBranch(cwd: string): string | null {
|
|
|
395
409
|
}
|
|
396
410
|
|
|
397
411
|
function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
|
|
398
|
-
spawnSync(
|
|
412
|
+
spawnSync(
|
|
413
|
+
plan.tmuxCommand,
|
|
414
|
+
["kill-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env: options.env })],
|
|
415
|
+
options,
|
|
416
|
+
);
|
|
399
417
|
}
|
|
400
418
|
function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
|
|
401
419
|
if (result.signalCode === "SIGHUP") return true;
|
|
@@ -499,7 +517,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
499
517
|
};
|
|
500
518
|
|
|
501
519
|
if (plan.attachSessionName) {
|
|
502
|
-
const attached = spawnSync(
|
|
520
|
+
const attached = spawnSync(
|
|
521
|
+
plan.tmuxCommand,
|
|
522
|
+
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.attachSessionName, { env })],
|
|
523
|
+
options,
|
|
524
|
+
);
|
|
503
525
|
if (attached.exitCode === 0) return true;
|
|
504
526
|
}
|
|
505
527
|
|
|
@@ -510,7 +532,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
510
532
|
buildGjcTmuxWindowTitle(plan.project ?? plan.cwd, plan.branch),
|
|
511
533
|
spawnSync,
|
|
512
534
|
options,
|
|
513
|
-
|
|
535
|
+
buildGjcTmuxExactSessionTarget(plan.sessionName, { env }),
|
|
514
536
|
);
|
|
515
537
|
|
|
516
538
|
const profile = applyGjcTmuxProfile({
|
|
@@ -535,7 +557,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
535
557
|
}
|
|
536
558
|
}
|
|
537
559
|
if (created.exitCode !== 0) return false;
|
|
538
|
-
const attached = spawnSync(
|
|
560
|
+
const attached = spawnSync(
|
|
561
|
+
plan.tmuxCommand,
|
|
562
|
+
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
563
|
+
options,
|
|
564
|
+
);
|
|
539
565
|
if (attached.exitCode === 0) return true;
|
|
540
566
|
if (isTmuxAttachDisconnectError(attached)) {
|
|
541
567
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
|
|
@@ -118,6 +118,20 @@ function findWorktreeByPath(entries: GitWorktreeEntry[], worktreePath: string):
|
|
|
118
118
|
return entries.find(entry => path.resolve(entry.path) === resolved) ?? null;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function describeWorktreeEntry(entry: GitWorktreeEntry): string {
|
|
122
|
+
return entry.detached ? `detached HEAD ${entry.head}` : (entry.branchRef ?? `HEAD ${entry.head}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatWorktreeTargetMismatch(plan: GjcLaunchWorktreePlan, existing: GitWorktreeEntry): string {
|
|
126
|
+
const expected = plan.detached ? `detached HEAD ${plan.baseRef}` : `branch refs/heads/${plan.branchName ?? ""}`;
|
|
127
|
+
return [
|
|
128
|
+
`worktree_target_mismatch:${plan.worktreePath}`,
|
|
129
|
+
`GJC launch worktree target is already registered for ${describeWorktreeEntry(existing)}, but this launch expects ${expected}.`,
|
|
130
|
+
`Path: ${plan.worktreePath}`,
|
|
131
|
+
"Refusing to delete or reuse the conflicting worktree automatically. Safe remediation: inspect the path, commit/stash any work, then remove or prune the stale worktree with git worktree remove <path> when it is no longer needed, or choose a different --worktree name.",
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
function hasBranchInUse(entries: GitWorktreeEntry[], branchName: string, worktreePath: string): boolean {
|
|
122
136
|
const expectedRef = `refs/heads/${branchName}`;
|
|
123
137
|
const resolvedPath = path.resolve(worktreePath);
|
|
@@ -228,7 +242,7 @@ export function ensureLaunchWorktree(
|
|
|
228
242
|
let dirty = isWorktreeDirty(plan.worktreePath);
|
|
229
243
|
if (plan.detached) {
|
|
230
244
|
if (!existingAtPath.detached) {
|
|
231
|
-
throw new Error(
|
|
245
|
+
throw new Error(formatWorktreeTargetMismatch(plan, existingAtPath));
|
|
232
246
|
}
|
|
233
247
|
if (existingAtPath.head !== plan.baseRef) {
|
|
234
248
|
if (dirty) throw new Error(`worktree_dirty:${plan.worktreePath}`);
|
|
@@ -236,7 +250,7 @@ export function ensureLaunchWorktree(
|
|
|
236
250
|
dirty = false;
|
|
237
251
|
}
|
|
238
252
|
} else if (existingAtPath.branchRef !== expectedBranchRef) {
|
|
239
|
-
throw new Error(
|
|
253
|
+
throw new Error(formatWorktreeTargetMismatch(plan, existingAtPath));
|
|
240
254
|
}
|
|
241
255
|
return {
|
|
242
256
|
...plan,
|
|
@@ -1896,13 +1896,27 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
|
|
|
1896
1896
|
}
|
|
1897
1897
|
return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
|
|
1898
1898
|
}
|
|
1899
|
-
export function resolveGjcWorkerCommand(
|
|
1899
|
+
export function resolveGjcWorkerCommand(
|
|
1900
|
+
cwd = process.cwd(),
|
|
1901
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1902
|
+
platform: NodeJS.Platform = process.platform,
|
|
1903
|
+
argv: string[] = process.argv,
|
|
1904
|
+
execPath = process.execPath,
|
|
1905
|
+
): string {
|
|
1900
1906
|
const explicit = env.GJC_TEAM_WORKER_COMMAND?.trim();
|
|
1901
1907
|
if (explicit) return explicit;
|
|
1902
|
-
const entrypoint =
|
|
1903
|
-
if (entrypoint
|
|
1904
|
-
|
|
1905
|
-
|
|
1908
|
+
const entrypoint = argv[1];
|
|
1909
|
+
if (!entrypoint) return "gjc";
|
|
1910
|
+
const pathModule = platform === "win32" ? path.win32 : path;
|
|
1911
|
+
const resolvedEntrypoint = pathModule.isAbsolute(entrypoint) ? entrypoint : pathModule.resolve(cwd, entrypoint);
|
|
1912
|
+
if (platform === "win32") {
|
|
1913
|
+
if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs"))
|
|
1914
|
+
return `${powershellQuote(execPath)} ${powershellQuote(resolvedEntrypoint)}`;
|
|
1915
|
+
if (pathModule.basename(entrypoint).startsWith("gjc")) return powershellQuote(resolvedEntrypoint);
|
|
1916
|
+
return "gjc";
|
|
1917
|
+
}
|
|
1918
|
+
if (entrypoint.endsWith(".ts")) return `${shellQuote(execPath)} ${shellQuote(resolvedEntrypoint)}`;
|
|
1919
|
+
if (path.basename(entrypoint).startsWith("gjc")) return shellQuote(path.resolve(cwd, entrypoint));
|
|
1906
1920
|
return "gjc";
|
|
1907
1921
|
}
|
|
1908
1922
|
/** @internal Exported for unit tests. */
|
|
@@ -1937,7 +1951,8 @@ export function buildWorkerCommand(
|
|
|
1937
1951
|
...(worker.worktree_path ? [envAssignment("GJC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
|
|
1938
1952
|
];
|
|
1939
1953
|
const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
|
|
1940
|
-
|
|
1954
|
+
const invocation = platform === "win32" ? `& ${config.worker_command}` : config.worker_command;
|
|
1955
|
+
return `${joined} ${invocation} ${quote(prompt)}`;
|
|
1941
1956
|
}
|
|
1942
1957
|
interface GjcTeamInitialLane {
|
|
1943
1958
|
label: string;
|
|
@@ -86,6 +86,22 @@ export function buildGjcTmuxExactOptionTarget(
|
|
|
86
86
|
return `=${sessionName}:`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Build the exact-session target for tmux *session-scoped* commands such as
|
|
91
|
+
* `attach-session` and `kill-session`. Native tmux accepts `=NAME` for an
|
|
92
|
+
* exact session match, but Windows psmux 3.3.x rejects that target form for
|
|
93
|
+
* session commands even though the bare `NAME` resolves. Keep native tmux on
|
|
94
|
+
* exact targets and intentionally use the bare session name for psmux.
|
|
95
|
+
*/
|
|
96
|
+
export function buildGjcTmuxExactSessionTarget(
|
|
97
|
+
sessionName: string,
|
|
98
|
+
opts: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; binary?: ResolvedTmuxBinary } = {},
|
|
99
|
+
): string {
|
|
100
|
+
const binary = opts.binary ?? resolveGjcTmuxBinary({ env: opts.env, platform: opts.platform });
|
|
101
|
+
if (binary.isPsmux) return sessionName;
|
|
102
|
+
return `=${sessionName}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
export const GJC_TMUX_UNTAGGED_REASON = "gjc_tmux_session_untagged";
|
|
90
106
|
|
|
91
107
|
export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
|