@triflux/core 10.87.0 → 10.88.1

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/bridge.mjs CHANGED
@@ -3,7 +3,17 @@
3
3
  // hub/bridge.mjs: tfx-route.sh 와 tfx-live 가 부르는 로컬 제어 CLI.
4
4
  // 팀 하위 명령은 nativeProxy 를 직접 호출하고 Claude daemon, retry, intervene 은 로컬 모듈만 쓴다.
5
5
 
6
- import { readFileSync } from "node:fs";
6
+ import {
7
+ closeSync,
8
+ existsSync,
9
+ fstatSync,
10
+ openSync,
11
+ readFileSync,
12
+ readSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
7
17
  import { parseArgs as nodeParseArgs } from "node:util";
8
18
 
9
19
  import {
@@ -46,6 +56,17 @@ export function parseArgs(argv) {
46
56
  "snapshot-file": { type: "string" },
47
57
  event: { type: "string" },
48
58
  "max-iterations": { type: "string" },
59
+ "stdout-log": { type: "string" },
60
+ "stderr-log": { type: "string" },
61
+ "rollout-file": { type: "string" },
62
+ "elapsed-sec": { type: "string" },
63
+ "result-file": { type: "string" },
64
+ agent: { type: "string" },
65
+ "cli-type": { type: "string" },
66
+ "available-servers": { type: "string" },
67
+ prompt: { type: "string" },
68
+ instruction: { type: "string" },
69
+ delimited: { type: "boolean" },
49
70
  },
50
71
  allowPositionals: true,
51
72
  strict: false,
@@ -151,14 +172,35 @@ async function cmdTeamTaskUpdate(args) {
151
172
 
152
173
  async function cmdTeamSendMessage(args) {
153
174
  const { teamSendMessage } = await import("./team/nativeProxy.mjs");
175
+ const { isJevEnabled, judgeInboxPriority } = await import(
176
+ "./lib/jev-addon.mjs"
177
+ );
178
+ let summary = args.summary;
179
+ let priority = null;
180
+ let urgencyScore = null;
181
+ if (isJevEnabled()) {
182
+ const verdict = await judgeInboxPriority({
183
+ text: args.text,
184
+ summary: args.summary,
185
+ from: args.from,
186
+ });
187
+ if (verdict) {
188
+ priority = verdict.priority;
189
+ urgencyScore = verdict.urgencyScore;
190
+ if (summary && !String(summary).startsWith("[JEV:")) {
191
+ summary = `[JEV:${verdict.priority}] ${summary}`;
192
+ }
193
+ }
194
+ }
154
195
  return emitJson(
155
196
  await teamSendMessage({
156
197
  team_name: args.team,
157
198
  from: args.from,
158
199
  to: args.to || "team-lead",
159
200
  text: args.text,
160
- summary: args.summary,
201
+ summary,
161
202
  color: args.color || "blue",
203
+ ...(priority ? { priority, urgencyScore } : {}),
162
204
  }),
163
205
  );
164
206
  }
@@ -706,7 +748,7 @@ async function cmdRetryRun(args) {
706
748
  result = sm.reportVerifySuccess();
707
749
  break;
708
750
  case "verify-fail":
709
- result = sm.reportVerifyFail(reason || "unspecified");
751
+ result = await sm.reportVerifyFailAsync(reason || "unspecified");
710
752
  break;
711
753
  default:
712
754
  console.error(`unknown --event: ${event}`);
@@ -781,6 +823,7 @@ async function cmdInterveneRun(args) {
781
823
  codexHome: payload.codexHome,
782
824
  });
783
825
  }
826
+ let reinstructPrompt = payload.reinstructPrompt || undefined;
784
827
  const seconds = (value) => {
785
828
  const parsed = numericOption(value, undefined);
786
829
  return parsed > 0 ? parsed * 1_000 : undefined;
@@ -791,6 +834,54 @@ async function cmdInterveneRun(args) {
791
834
  ),
792
835
  rolloutFile,
793
836
  });
837
+ const { isJevEnabled, judgeWorkerHealth } = await import(
838
+ "./lib/jev-addon.mjs"
839
+ );
840
+ if (isJevEnabled() && payload.skipJevHealthCheck !== true) {
841
+ const stdoutTail = readTailSafe(payload.stdoutLog);
842
+ const stderrTail = readTailSafe(payload.stderrLog);
843
+ const rolloutTail = readTailSafe(rolloutFile);
844
+ const activitySig = await Promise.resolve(readActivitySignature()).catch(
845
+ () => "",
846
+ );
847
+ const graceTargetKey = String(
848
+ payload.pid ||
849
+ payload.paneId ||
850
+ payload.sessionId ||
851
+ payload.stdoutLog ||
852
+ payload.stderrLog ||
853
+ rolloutFile ||
854
+ "default",
855
+ );
856
+ const logSignature = `${activitySig}|${stdoutTail.length}:${stderrTail.length}:${rolloutTail.length}|${stdoutTail.slice(-160)}|${stderrTail.slice(-160)}|${rolloutTail.slice(-160)}`;
857
+ const alreadyGranted = hasGrantedProgressingGrace(
858
+ graceTargetKey,
859
+ logSignature,
860
+ );
861
+ const verdict = await judgeWorkerHealth({
862
+ stdoutTail,
863
+ stderrTail,
864
+ rolloutTail,
865
+ elapsedSec: numericOption(payload.elapsedSec, 0),
866
+ });
867
+ if (verdict?.status === "progressing" && !alreadyGranted) {
868
+ recordProgressingGrace(graceTargetKey, logSignature);
869
+ return emitJson({
870
+ ok: true,
871
+ outcome: "recovered",
872
+ step: "jev-health-check",
873
+ status: "progressing",
874
+ confidence: verdict.confidence,
875
+ });
876
+ }
877
+ if (!reinstructPrompt && verdict?.status === "waiting_input") {
878
+ reinstructPrompt =
879
+ "Continue non-interactively using safe defaults without waiting for interactive user input.";
880
+ } else if (!reinstructPrompt && verdict?.status === "looping_error") {
881
+ reinstructPrompt =
882
+ "Stop repeating the failing command. Summarize the root cause and switch to an alternative approach.";
883
+ }
884
+ }
794
885
  const ladder = intervention.createInterventionLadder({
795
886
  target: {
796
887
  channel: payload.channel,
@@ -803,7 +894,7 @@ async function cmdInterveneRun(args) {
803
894
  rolloutFile,
804
895
  daemon: payload.daemon,
805
896
  },
806
- reinstructPrompt: payload.reinstructPrompt || undefined,
897
+ reinstructPrompt,
807
898
  readActivitySignature,
808
899
  config: {
809
900
  ...(seconds(payload.reinstructWaitSec)
@@ -830,6 +921,307 @@ async function cmdInterveneRun(args) {
830
921
  }
831
922
  }
832
923
 
924
+ const PROGRESSING_GRACE_MEM = new Map();
925
+
926
+ function progressingGraceFilePath() {
927
+ return (
928
+ process.env.TFX_JEV_GRACE_FILE ||
929
+ join(tmpdir(), "tfx-jev-progressing-grace.json")
930
+ );
931
+ }
932
+
933
+ function loadProgressingGraceStore() {
934
+ try {
935
+ const file = progressingGraceFilePath();
936
+ if (!existsSync(file)) return {};
937
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
938
+ return parsed && typeof parsed === "object" ? parsed : {};
939
+ } catch {
940
+ return {};
941
+ }
942
+ }
943
+
944
+ function hasGrantedProgressingGrace(targetKey, logSignature) {
945
+ if (!targetKey || !logSignature) return false;
946
+ if (PROGRESSING_GRACE_MEM.get(targetKey) === logSignature) return true;
947
+ const store = loadProgressingGraceStore();
948
+ return store[targetKey] === logSignature;
949
+ }
950
+
951
+ function recordProgressingGrace(targetKey, logSignature) {
952
+ if (!targetKey || !logSignature) return;
953
+ PROGRESSING_GRACE_MEM.set(targetKey, logSignature);
954
+ try {
955
+ const store = loadProgressingGraceStore();
956
+ store[targetKey] = logSignature;
957
+ const keys = Object.keys(store);
958
+ if (keys.length > 64) {
959
+ for (const oldKey of keys.slice(0, keys.length - 64)) {
960
+ delete store[oldKey];
961
+ }
962
+ }
963
+ writeFileSync(progressingGraceFilePath(), JSON.stringify(store), "utf8");
964
+ } catch {
965
+ // 상태 기록 실패는 무시
966
+ }
967
+ }
968
+
969
+ export function readTailSafe(filePath, maxBytes = 2000) {
970
+ if (!filePath) return "";
971
+ let fd;
972
+ try {
973
+ fd = openSync(String(filePath), "r");
974
+ const { size } = fstatSync(fd);
975
+ if (size <= 0) return "";
976
+ const readLen = Math.min(size, maxBytes);
977
+ const offset = Math.max(0, size - readLen);
978
+ const buf = Buffer.allocUnsafe(readLen);
979
+ const bytesRead = readSync(fd, buf, 0, readLen, offset);
980
+ return buf
981
+ .subarray(0, bytesRead)
982
+ .toString("utf8")
983
+ .replace(/^\uFFFD+/, "");
984
+ } catch {
985
+ return "";
986
+ } finally {
987
+ if (fd !== undefined) {
988
+ try {
989
+ closeSync(fd);
990
+ } catch {}
991
+ }
992
+ }
993
+ }
994
+
995
+ async function cmdJudgeStall(args) {
996
+ try {
997
+ const payload =
998
+ args["payload-file"] || args.payload ? readBridgePayload(args) : {};
999
+ const stdoutLog = args["stdout-log"] ?? payload.stdoutLog;
1000
+ const stderrLog = args["stderr-log"] ?? payload.stderrLog;
1001
+ const rolloutFile = args["rollout-file"] ?? payload.rolloutFile;
1002
+ const elapsedSec = Number(args["elapsed-sec"] ?? payload.elapsedSec) || 0;
1003
+ const { isJevEnabled, judgeWorkerHealth } = await import(
1004
+ "./lib/jev-addon.mjs"
1005
+ );
1006
+ if (!isJevEnabled()) {
1007
+ return emitJson({ ok: true, enabled: false, status: null });
1008
+ }
1009
+ const verdict = await judgeWorkerHealth({
1010
+ stdoutTail: readTailSafe(stdoutLog),
1011
+ stderrTail: readTailSafe(stderrLog),
1012
+ rolloutTail: readTailSafe(rolloutFile),
1013
+ elapsedSec,
1014
+ });
1015
+ return emitJson({
1016
+ ok: true,
1017
+ enabled: true,
1018
+ status: verdict?.status ?? null,
1019
+ confidence: verdict?.confidence ?? null,
1020
+ });
1021
+ } catch (error) {
1022
+ return emitJson({
1023
+ ok: false,
1024
+ enabled: false,
1025
+ status: null,
1026
+ error: error?.message || String(error),
1027
+ });
1028
+ }
1029
+ }
1030
+
1031
+ async function cmdJevBatch(args) {
1032
+ try {
1033
+ const payload = readBridgePayload(args);
1034
+ const state = String(payload.state ?? payload.context ?? "");
1035
+ const questions =
1036
+ payload.questions && typeof payload.questions === "object"
1037
+ ? payload.questions
1038
+ : {};
1039
+ const { isJevEnabled, queryJevBatch } = await import("./lib/jev-addon.mjs");
1040
+ if (!isJevEnabled()) {
1041
+ return emitJson({
1042
+ ok: true,
1043
+ enabled: false,
1044
+ answers: null,
1045
+ });
1046
+ }
1047
+ const answers = await queryJevBatch({ state, questions });
1048
+ return emitJson({ ok: true, enabled: true, answers });
1049
+ } catch (error) {
1050
+ return emitJson({
1051
+ ok: false,
1052
+ enabled: false,
1053
+ answers: null,
1054
+ error: error?.message || String(error),
1055
+ });
1056
+ }
1057
+ }
1058
+
1059
+ async function cmdJevStats() {
1060
+ try {
1061
+ const { isJevEnabled, summarizeJevStats } = await import(
1062
+ "./lib/jev-addon.mjs"
1063
+ );
1064
+ const stats = summarizeJevStats();
1065
+ return emitJson({ ok: true, enabled: isJevEnabled(), stats });
1066
+ } catch (error) {
1067
+ return emitJson({
1068
+ ok: false,
1069
+ error: error?.message || String(error),
1070
+ });
1071
+ }
1072
+ }
1073
+
1074
+ async function cmdHandoffCheck(args) {
1075
+ try {
1076
+ const payload =
1077
+ args["payload-file"] || args.payload ? readBridgePayload(args) : {};
1078
+ const resultFile = args["result-file"] ?? payload.resultFile;
1079
+ const rawText =
1080
+ args.text ??
1081
+ payload.rawText ??
1082
+ (resultFile && existsSync(resultFile)
1083
+ ? readFileSync(resultFile, "utf8")
1084
+ : "");
1085
+ const { isJevEnabled, judgeHandoffQuality } = await import(
1086
+ "./lib/jev-addon.mjs"
1087
+ );
1088
+ if (!isJevEnabled()) {
1089
+ return emitJson({ ok: true, enabled: false, verdict: null });
1090
+ }
1091
+ const verdict = await judgeHandoffQuality({
1092
+ rawText,
1093
+ handoff: payload.handoff ?? {
1094
+ status: payload.status ?? "ok",
1095
+ lead_action: payload.leadAction ?? "accept",
1096
+ files_changed: Array.isArray(payload.gitDiffFiles)
1097
+ ? payload.gitDiffFiles
1098
+ : [],
1099
+ verdict: payload.verdict ?? "",
1100
+ },
1101
+ gitDiffFiles: Array.isArray(payload.gitDiffFiles)
1102
+ ? payload.gitDiffFiles
1103
+ : [],
1104
+ });
1105
+ return emitJson({ ok: true, enabled: true, verdict });
1106
+ } catch (error) {
1107
+ return emitJson({
1108
+ ok: false,
1109
+ error: error?.message || String(error),
1110
+ });
1111
+ }
1112
+ }
1113
+
1114
+ async function cmdJudgeRoute(args) {
1115
+ try {
1116
+ const payload =
1117
+ args["payload-file"] || args.payload ? readBridgePayload(args) : {};
1118
+ const prompt = String(args.prompt ?? args.text ?? payload.prompt ?? "");
1119
+ const agentType = String(args.agent ?? payload.agentType ?? "executor");
1120
+ const cliType = String(args["cli-type"] ?? payload.cliType ?? "codex");
1121
+ const availableRaw =
1122
+ args["available-servers"] ?? payload.availableServers ?? "";
1123
+ const availableServers = Array.isArray(availableRaw)
1124
+ ? availableRaw
1125
+ : String(availableRaw)
1126
+ .split(",")
1127
+ .map((s) => s.trim())
1128
+ .filter(Boolean);
1129
+
1130
+ const { isJevEnabled, judgeTaskRouting } = await import(
1131
+ "./lib/jev-addon.mjs"
1132
+ );
1133
+ if (!isJevEnabled()) {
1134
+ if (args.delimited) return true;
1135
+ return emitJson({ ok: true, enabled: false, routing: null });
1136
+ }
1137
+ const routing = await judgeTaskRouting({
1138
+ prompt,
1139
+ agentType,
1140
+ cliType,
1141
+ availableServers,
1142
+ });
1143
+ if (args.delimited) {
1144
+ if (!routing) return true;
1145
+ const RS = "\x1e";
1146
+ const fields = [
1147
+ routing.complexity || "",
1148
+ routing.codexProfile || "",
1149
+ routing.agyProfile || "",
1150
+ routing.claudeModel || "",
1151
+ routing.claudeEffort || "",
1152
+ String(routing.expectedDurationSec || ""),
1153
+ routing.executionMode || "",
1154
+ routing.mcpProfile || "",
1155
+ Array.isArray(routing.neededMcpServers)
1156
+ ? routing.neededMcpServers.join(",")
1157
+ : "",
1158
+ String(routing.confidence ?? ""),
1159
+ ];
1160
+ process.stdout.write(fields.join(RS));
1161
+ return true;
1162
+ }
1163
+ return emitJson({ ok: true, enabled: true, routing });
1164
+ } catch (error) {
1165
+ if (args.delimited) return false;
1166
+ return emitJson({
1167
+ ok: false,
1168
+ enabled: false,
1169
+ routing: null,
1170
+ error: error?.message || String(error),
1171
+ });
1172
+ }
1173
+ }
1174
+
1175
+ async function cmdJudgeAck(args) {
1176
+ try {
1177
+ const payload =
1178
+ args["payload-file"] || args.payload ? readBridgePayload(args) : {};
1179
+ const instruction = String(args.instruction ?? payload.instruction ?? "");
1180
+ const stdoutLog = args["stdout-log"] ?? payload.stdoutLog;
1181
+ const recentOutput = String(
1182
+ args.text ?? payload.recentOutput ?? readTailSafe(stdoutLog),
1183
+ );
1184
+ const { isJevEnabled, judgeInstructionAck } = await import(
1185
+ "./lib/jev-addon.mjs"
1186
+ );
1187
+ if (!isJevEnabled()) {
1188
+ return emitJson({ ok: true, enabled: false, verdict: null });
1189
+ }
1190
+ const verdict = await judgeInstructionAck({ instruction, recentOutput });
1191
+ return emitJson({ ok: true, enabled: true, verdict });
1192
+ } catch (error) {
1193
+ return emitJson({
1194
+ ok: false,
1195
+ error: error?.message || String(error),
1196
+ });
1197
+ }
1198
+ }
1199
+
1200
+ async function cmdJudgeReview(args) {
1201
+ try {
1202
+ const payload =
1203
+ args["payload-file"] || args.payload ? readBridgePayload(args) : {};
1204
+ const { isJevEnabled, judgeReviewGate } = await import(
1205
+ "./lib/jev-addon.mjs"
1206
+ );
1207
+ if (!isJevEnabled()) {
1208
+ return emitJson({ ok: true, enabled: false, verdict: null });
1209
+ }
1210
+ const verdict = await judgeReviewGate({
1211
+ diffStat: String(payload.diffStat ?? ""),
1212
+ handoffSummary: String(args.summary ?? payload.handoffSummary ?? ""),
1213
+ previousReviews: String(payload.previousReviews ?? ""),
1214
+ currentReview: String(args.text ?? payload.currentReview ?? ""),
1215
+ });
1216
+ return emitJson({ ok: true, enabled: true, verdict });
1217
+ } catch (error) {
1218
+ return emitJson({
1219
+ ok: false,
1220
+ error: error?.message || String(error),
1221
+ });
1222
+ }
1223
+ }
1224
+
833
1225
  export async function main(argv = process.argv.slice(2)) {
834
1226
  const cmd = argv[0];
835
1227
  const args = parseArgs(argv.slice(1));
@@ -857,9 +1249,23 @@ export async function main(argv = process.argv.slice(2)) {
857
1249
  return await cmdRetryStatus(args);
858
1250
  case "intervene-run":
859
1251
  return await cmdInterveneRun(args);
1252
+ case "judge-stall":
1253
+ return await cmdJudgeStall(args);
1254
+ case "jev-batch":
1255
+ return await cmdJevBatch(args);
1256
+ case "jev-stats":
1257
+ return await cmdJevStats(args);
1258
+ case "handoff-check":
1259
+ return await cmdHandoffCheck(args);
1260
+ case "judge-route":
1261
+ return await cmdJudgeRoute(args);
1262
+ case "judge-ack":
1263
+ return await cmdJudgeAck(args);
1264
+ case "judge-review":
1265
+ return await cmdJudgeReview(args);
860
1266
  default:
861
1267
  console.error(
862
- "사용법: bridge.mjs <team-info|team-task-list|team-task-update|team-send-message|daemon-probe|daemon-attach|daemon-wait|daemon-interrupt|retry-run|retry-status|intervene-run> [--옵션]",
1268
+ "사용법: bridge.mjs <team-info|team-task-list|team-task-update|team-send-message|daemon-probe|daemon-attach|daemon-wait|daemon-interrupt|retry-run|retry-status|intervene-run|judge-stall|jev-batch|jev-stats|handoff-check|judge-route|judge-ack|judge-review> [--옵션]",
863
1269
  );
864
1270
  process.exit(1);
865
1271
  }