@triflux/remote 10.52.0 → 10.53.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/hub/team/claude-transcript.mjs +19 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/headless.mjs +36 -8
- package/hub/team/results-index.mjs +92 -0
- package/hub/team/uds-orchestrator.mjs +18 -19
- package/package.json +1 -1
- package/scripts/lib/mcp-guard-engine.mjs +2 -3
|
@@ -157,6 +157,25 @@ function acceptLine(state, line, marker) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// daemon 과 tmux wait 가 같은 기준으로 끝을 판정한다. 아직이면 null.
|
|
161
|
+
export function claudeWaitVerdict(transcript, idle) {
|
|
162
|
+
if (transcript?.error)
|
|
163
|
+
return { ok: false, status: "failed", error: transcript.error };
|
|
164
|
+
if (
|
|
165
|
+
!transcript?.userSeen ||
|
|
166
|
+
!transcript.turnEnded ||
|
|
167
|
+
!(idle || transcript.sectionClosed)
|
|
168
|
+
)
|
|
169
|
+
return null;
|
|
170
|
+
if (!transcript.response.trim())
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
status: "failed",
|
|
174
|
+
error: "turn ended without assistant text",
|
|
175
|
+
};
|
|
176
|
+
return { status: "completed", done: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
160
179
|
export async function readClaudeTranscript(transcriptPath, { requestId } = {}) {
|
|
161
180
|
if (!transcriptPath) return null;
|
|
162
181
|
const marker = requestId ? `[tfx-live req=${requestId}]` : null;
|
|
@@ -173,6 +173,8 @@ export async function startHeadlessTeam({
|
|
|
173
173
|
console.log(
|
|
174
174
|
`=== HEADLESS_COMPLETE succeeded=${succeeded.length} failed=${failed.length} total=${results.length} ===`,
|
|
175
175
|
);
|
|
176
|
+
if (handle.resultsIndexPath)
|
|
177
|
+
console.log(` ${DIM}결과 색인: ${handle.resultsIndexPath}${RESET}`);
|
|
176
178
|
|
|
177
179
|
if (failed.length > 0) {
|
|
178
180
|
for (const r of failed)
|
package/hub/team/headless.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
startCapture,
|
|
44
44
|
waitForCompletion,
|
|
45
45
|
} from "./psmux.mjs";
|
|
46
|
+
import { createResultsIndex, resultsIndexPath } from "./results-index.mjs";
|
|
46
47
|
import { createLogDashboard } from "./tui.mjs";
|
|
47
48
|
import { createWtManager } from "./wt-manager.mjs";
|
|
48
49
|
|
|
@@ -126,6 +127,14 @@ export function normalizeHeadlessRole(role) {
|
|
|
126
127
|
return normalized && VALID_ROUTE_AGENTS.has(normalized) ? normalized : "";
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
export function headlessResultFile(sessionName, paneName) {
|
|
131
|
+
return join(RESULT_DIR, `${sessionName}-${paneName}.txt`).replace(/\\/g, "/");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function headlessResultsIndexPath(sessionName) {
|
|
135
|
+
return resultsIndexPath(RESULT_DIR, sessionName).replace(/\\/g, "/");
|
|
136
|
+
}
|
|
137
|
+
|
|
129
138
|
export function resolveHeadlessDisplayName(assignment = {}, paneName = "") {
|
|
130
139
|
const normalized = String(assignment.displayName || "")
|
|
131
140
|
.trim()
|
|
@@ -543,10 +552,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
543
552
|
});
|
|
544
553
|
|
|
545
554
|
// 캡처 시작 + 컬러 배너 + 명령 dispatch
|
|
546
|
-
const resultFile =
|
|
547
|
-
RESULT_DIR,
|
|
548
|
-
`${sessionName}-${paneName}.txt`,
|
|
549
|
-
).replace(/\\/g, "/");
|
|
555
|
+
const resultFile = headlessResultFile(sessionName, paneName);
|
|
550
556
|
// Issue #118 review R1 HIGH: stale artifact 제거 (이전 run / restart 잔재)
|
|
551
557
|
cleanStaleResultArtifacts(resultFile);
|
|
552
558
|
const cmd = buildHeadlessCommand(
|
|
@@ -634,10 +640,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
634
640
|
const displayName = resolveHeadlessDisplayName(assignment, paneName);
|
|
635
641
|
const workerId = getHeadlessWorkerAgentId(sessionName, i);
|
|
636
642
|
const resolvedCli = resolveCliType(assignment.cli);
|
|
637
|
-
const resultFile =
|
|
638
|
-
RESULT_DIR,
|
|
639
|
-
`${sessionName}-${paneName}.txt`,
|
|
640
|
-
).replace(/\\/g, "/");
|
|
643
|
+
const resultFile = headlessResultFile(sessionName, paneName);
|
|
641
644
|
// Issue #118 review R1 HIGH: stale artifact 제거 (이전 run / restart 잔재)
|
|
642
645
|
cleanStaleResultArtifacts(resultFile);
|
|
643
646
|
const cmd = buildHeadlessCommand(
|
|
@@ -918,6 +921,23 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
918
921
|
return { sessionName, results: [], sessionOwnership, agentsRows };
|
|
919
922
|
}
|
|
920
923
|
|
|
924
|
+
const resultsIndex = createResultsIndex(
|
|
925
|
+
headlessResultsIndexPath(sessionName),
|
|
926
|
+
sessionName,
|
|
927
|
+
normalizedAssignments.map((a, i) => {
|
|
928
|
+
const paneName = `worker-${i + 1}`;
|
|
929
|
+
return {
|
|
930
|
+
paneName,
|
|
931
|
+
displayName: a.displayName,
|
|
932
|
+
cli: resolveCliType(a.cli),
|
|
933
|
+
role: a.role,
|
|
934
|
+
resultFile: headlessResultFile(sessionName, paneName),
|
|
935
|
+
};
|
|
936
|
+
}),
|
|
937
|
+
// 같은 이름의 남의 세션 색인을 덮어쓰지 않도록 세션을 소유한 동안만 쓴다.
|
|
938
|
+
() => sessionOwnership.owned,
|
|
939
|
+
);
|
|
940
|
+
|
|
921
941
|
let runCompleted = false;
|
|
922
942
|
let completedResults = [];
|
|
923
943
|
let runFailed = false;
|
|
@@ -996,6 +1016,11 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
996
1016
|
|
|
997
1017
|
// onProgress 예외를 삼켜 실행 흐름 보호 (onPoll과 동일 패턴)
|
|
998
1018
|
const combinedProgress = (event) => {
|
|
1019
|
+
if (event.type === "session_created") resultsIndex.begin();
|
|
1020
|
+
else if (event.type === "dispatched")
|
|
1021
|
+
resultsIndex.workerStarted(event.paneName);
|
|
1022
|
+
else if (event.type === "completed")
|
|
1023
|
+
resultsIndex.workerFinished(event.paneName, event);
|
|
999
1024
|
feedTui(event);
|
|
1000
1025
|
if (onProgress) {
|
|
1001
1026
|
try {
|
|
@@ -1077,6 +1102,7 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1077
1102
|
runFailed = true;
|
|
1078
1103
|
runError = error;
|
|
1079
1104
|
}
|
|
1105
|
+
resultsIndex.finish(runCompleted);
|
|
1080
1106
|
|
|
1081
1107
|
if (!runCompleted) {
|
|
1082
1108
|
sessionOwnership.release();
|
|
@@ -1086,6 +1112,7 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
1086
1112
|
return {
|
|
1087
1113
|
sessionName,
|
|
1088
1114
|
results: completedResults,
|
|
1115
|
+
resultsIndexPath: resultsIndex.path,
|
|
1089
1116
|
sessionOwnership,
|
|
1090
1117
|
agentsRows,
|
|
1091
1118
|
};
|
|
@@ -1638,6 +1665,7 @@ export async function runHeadlessInteractive(
|
|
|
1638
1665
|
const handle = {
|
|
1639
1666
|
sessionName,
|
|
1640
1667
|
results,
|
|
1668
|
+
resultsIndexPath: runResult.resultsIndexPath,
|
|
1641
1669
|
dispatches,
|
|
1642
1670
|
_killed: false,
|
|
1643
1671
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// headless 세션 결과 색인. 화면의 HEADLESS_COMPLETE 문자열 대신 읽도록 세션당 JSON 하나를 둔다 (ADR-0034).
|
|
2
|
+
import { renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const RESULTS_INDEX_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export function resultsIndexPath(dir, sessionName) {
|
|
8
|
+
return join(dir, `${sessionName}.results.json`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} path
|
|
13
|
+
* @param {string} sessionName
|
|
14
|
+
* @param {Array<{paneName: string, displayName?: string, cli: string, role?: string, resultFile: string}>} workers
|
|
15
|
+
* @param {() => boolean} canWrite
|
|
16
|
+
*/
|
|
17
|
+
export function createResultsIndex(path, sessionName, workers, canWrite) {
|
|
18
|
+
const now = () => new Date().toISOString();
|
|
19
|
+
const index = {
|
|
20
|
+
version: RESULTS_INDEX_VERSION,
|
|
21
|
+
sessionName,
|
|
22
|
+
startedAt: now(),
|
|
23
|
+
finishedAt: null,
|
|
24
|
+
completed: false,
|
|
25
|
+
workers: workers.map((w) => ({
|
|
26
|
+
paneName: w.paneName,
|
|
27
|
+
displayName: w.displayName || w.paneName,
|
|
28
|
+
cli: w.cli,
|
|
29
|
+
role: w.role || "",
|
|
30
|
+
status: "pending",
|
|
31
|
+
matched: null,
|
|
32
|
+
exitCode: null,
|
|
33
|
+
sessionDead: false,
|
|
34
|
+
startedAt: null,
|
|
35
|
+
finishedAt: null,
|
|
36
|
+
resultFile: w.resultFile,
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
const byPane = new Map(index.workers.map((w) => [w.paneName, w]));
|
|
40
|
+
|
|
41
|
+
// 임시 파일에 쓰고 rename 해 읽는 쪽이 반쯤 쓴 JSON 을 보지 않게 한다.
|
|
42
|
+
// 색인은 보조 산출물이라 쓰기 실패가 실행을 멈추면 안 된다.
|
|
43
|
+
const flush = () => {
|
|
44
|
+
if (!canWrite()) return;
|
|
45
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(tmp, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
48
|
+
renameSync(tmp, path);
|
|
49
|
+
} catch {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(tmp, { force: true });
|
|
52
|
+
} catch {
|
|
53
|
+
/* best-effort */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
path,
|
|
60
|
+
// 같은 이름의 이전 실행 색인이 남아 있으면 세션을 잡자마자 덮어 오판을 막는다.
|
|
61
|
+
begin: flush,
|
|
62
|
+
workerStarted(paneName) {
|
|
63
|
+
const w = byPane.get(paneName);
|
|
64
|
+
if (!w || w.status !== "pending") return;
|
|
65
|
+
w.status = "running";
|
|
66
|
+
w.startedAt = now();
|
|
67
|
+
flush();
|
|
68
|
+
},
|
|
69
|
+
workerFinished(paneName, { matched, exitCode, sessionDead } = {}) {
|
|
70
|
+
const w = byPane.get(paneName);
|
|
71
|
+
if (!w) return;
|
|
72
|
+
w.status = matched && exitCode === 0 ? "completed" : "failed";
|
|
73
|
+
w.matched = Boolean(matched);
|
|
74
|
+
w.exitCode = exitCode ?? null;
|
|
75
|
+
w.sessionDead = Boolean(sessionDead);
|
|
76
|
+
w.finishedAt = now();
|
|
77
|
+
flush();
|
|
78
|
+
},
|
|
79
|
+
finish(completed) {
|
|
80
|
+
// 예외로 끝나면 끝나지 못한 워커를 failed 로 확정해 "아직 도는 중"과 구분한다.
|
|
81
|
+
for (const w of index.workers) {
|
|
82
|
+
if (w.status === "pending" || w.status === "running") {
|
|
83
|
+
w.status = "failed";
|
|
84
|
+
w.finishedAt = now();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
index.finishedAt = now();
|
|
88
|
+
index.completed = Boolean(completed);
|
|
89
|
+
flush();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -15,12 +15,12 @@ function codexClient(socketPath, timeoutMs, clientFactory) {
|
|
|
15
15
|
return clientFactory({ socketPath, connectTimeoutMs: timeoutMs });
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async function initializeCodexClient(client, timeoutMs) {
|
|
18
|
+
async function initializeCodexClient(client, timeoutMs, remainingMs) {
|
|
19
19
|
await client.connect();
|
|
20
20
|
await client.request(
|
|
21
21
|
"initialize",
|
|
22
22
|
{ clientInfo: { name: "triflux-tfx-live", version: "1.0.0" } },
|
|
23
|
-
timeoutMs,
|
|
23
|
+
remainingMs ? Math.max(1, remainingMs()) : timeoutMs,
|
|
24
24
|
);
|
|
25
25
|
client.notify("initialized", {});
|
|
26
26
|
}
|
|
@@ -211,7 +211,14 @@ async function normalizedCwd(cwd) {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
// timeoutMs 는 RPC 하나의 상한이다. remainingMs 를 주면 조회 전체가 그 안에 끝나야 한다.
|
|
215
|
+
async function loadedCodexThreads(client, cwd, timeoutMs, remainingMs) {
|
|
216
|
+
const limit = () => {
|
|
217
|
+
if (!remainingMs) return timeoutMs;
|
|
218
|
+
const left = remainingMs();
|
|
219
|
+
if (left <= 0) throw new Error("codex thread listing timed out");
|
|
220
|
+
return left;
|
|
221
|
+
};
|
|
215
222
|
const wantedCwd = await normalizedCwd(cwd);
|
|
216
223
|
const threads = [];
|
|
217
224
|
let cursor;
|
|
@@ -219,10 +226,10 @@ async function loadedCodexThreads(client, cwd, timeoutMs) {
|
|
|
219
226
|
const result = await client.request(
|
|
220
227
|
"thread/loaded/list",
|
|
221
228
|
cursor ? { cursor } : {},
|
|
222
|
-
|
|
229
|
+
limit(),
|
|
223
230
|
);
|
|
224
231
|
for (const threadId of result?.data || []) {
|
|
225
|
-
const thread = await readCodexThread(client, threadId,
|
|
232
|
+
const thread = await readCodexThread(client, threadId, limit());
|
|
226
233
|
if (wantedCwd && (await normalizedCwd(thread.cwd)) !== wantedCwd)
|
|
227
234
|
continue;
|
|
228
235
|
threads.push({
|
|
@@ -241,23 +248,15 @@ async function loadedCodexThreads(client, cwd, timeoutMs) {
|
|
|
241
248
|
export async function listCodexAppServerThreads({
|
|
242
249
|
socketPath,
|
|
243
250
|
cwd,
|
|
251
|
+
timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
244
252
|
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
245
253
|
} = {}) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
clientFactory,
|
|
250
|
-
);
|
|
254
|
+
const deadline = Date.now() + timeoutMs;
|
|
255
|
+
const remainingMs = () => deadline - Date.now();
|
|
256
|
+
const client = codexClient(socketPath, timeoutMs, clientFactory);
|
|
251
257
|
try {
|
|
252
|
-
await initializeCodexClient(
|
|
253
|
-
|
|
254
|
-
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
255
|
-
);
|
|
256
|
-
return await loadedCodexThreads(
|
|
257
|
-
client,
|
|
258
|
-
cwd,
|
|
259
|
-
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
260
|
-
);
|
|
258
|
+
await initializeCodexClient(client, timeoutMs, remainingMs);
|
|
259
|
+
return await loadedCodexThreads(client, cwd, timeoutMs, remainingMs);
|
|
261
260
|
} finally {
|
|
262
261
|
client.close();
|
|
263
262
|
}
|
package/package.json
CHANGED
|
@@ -40,7 +40,6 @@ const DEFAULT_REGISTRY = Object.freeze({
|
|
|
40
40
|
unknown_server_action: "warn",
|
|
41
41
|
sync_denylist: [],
|
|
42
42
|
watched_paths: [
|
|
43
|
-
"~/.gemini/settings.json",
|
|
44
43
|
"~/.codex/config.toml",
|
|
45
44
|
"~/.claude.json",
|
|
46
45
|
"~/.claude/settings.json",
|
|
@@ -709,7 +708,7 @@ function serverTargets(serverConfig) {
|
|
|
709
708
|
),
|
|
710
709
|
];
|
|
711
710
|
}
|
|
712
|
-
return ["claude", "
|
|
711
|
+
return ["claude", "codex", "antigravity"];
|
|
713
712
|
}
|
|
714
713
|
|
|
715
714
|
function serverAppliesToClient(serverConfig, client) {
|
|
@@ -1718,7 +1717,7 @@ export function addRegistryServer(name, url, options = {}) {
|
|
|
1718
1717
|
.filter(Boolean),
|
|
1719
1718
|
),
|
|
1720
1719
|
]
|
|
1721
|
-
: ["claude", "
|
|
1720
|
+
: ["claude", "codex", "antigravity"],
|
|
1722
1721
|
description: options.description || `${trimmedName} MCP 서버`,
|
|
1723
1722
|
};
|
|
1724
1723
|
|