@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 +411 -5
- package/hub/lib/jev-addon.mjs +1675 -0
- package/hub/team/pane.mjs +99 -0
- package/hub/team/retry-state-machine.mjs +22 -2
- package/hud/hud-qos-status.mjs +45 -16
- package/hud/renderers.mjs +13 -3
- package/hud/session-status.mjs +46 -0
- package/hud/tmux-status.mjs +140 -0
- package/package.json +1 -1
- package/scripts/lib/doctor-env-checks.mjs +377 -0
- package/scripts/lib/mcp-filter.mjs +86 -3
|
@@ -0,0 +1,1675 @@
|
|
|
1
|
+
// hub/lib/jev-addon.mjs
|
|
2
|
+
// Opt-in Jev (System One) 판단 애드온.
|
|
3
|
+
// JEV_API_KEY / TYPESAFE_API_KEY 환경변수 또는 jev CLI(TFX_JEV=1)가 있을 때만 동작하고,
|
|
4
|
+
// 설정이 없거나 호출이 실패하면 항상 null을 반환해 기존 결정론적 로직을 그대로 유지한다.
|
|
5
|
+
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { whichCommand } from "../platform.mjs";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
|
|
16
|
+
export const DEFAULT_JEV_MODEL = "jev-latest";
|
|
17
|
+
export const DEFAULT_JEV_TIMEOUT_MS = 1500;
|
|
18
|
+
export const DEFAULT_JEV_CONFIDENCE_THRESHOLD = 0.7;
|
|
19
|
+
export const DEFAULT_JEV_STATS_PATH = join(
|
|
20
|
+
homedir(),
|
|
21
|
+
".triflux",
|
|
22
|
+
"logs",
|
|
23
|
+
"jev-stats.jsonl",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
let cachedCliBin;
|
|
27
|
+
|
|
28
|
+
function isExplicitlyDisabled(env = process.env) {
|
|
29
|
+
const flag = String(env.TFX_JEV ?? "")
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
return flag === "0" || flag === "false" || flag === "off";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isExplicitlyEnabled(env = process.env) {
|
|
36
|
+
const flag = String(env.TFX_JEV ?? "")
|
|
37
|
+
.trim()
|
|
38
|
+
.toLowerCase();
|
|
39
|
+
return flag === "1" || flag === "true" || flag === "on";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveJevApiKey(env = process.env) {
|
|
43
|
+
const key = env.JEV_API_KEY || env.TYPESAFE_API_KEY || "";
|
|
44
|
+
return String(key).trim() || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveJevCliBin(env = process.env) {
|
|
48
|
+
if (env.TFX_JEV_CLI) {
|
|
49
|
+
return String(env.TFX_JEV_CLI).trim() || null;
|
|
50
|
+
}
|
|
51
|
+
if (!isExplicitlyEnabled(env)) return null;
|
|
52
|
+
if (cachedCliBin !== undefined) return cachedCliBin;
|
|
53
|
+
cachedCliBin =
|
|
54
|
+
whichCommand("jev") || whichCommand("jev-cli") || whichCommand("jevctl");
|
|
55
|
+
return cachedCliBin || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resetJevAddonCache() {
|
|
59
|
+
cachedCliBin = undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isDirectEnvObject(obj) {
|
|
63
|
+
if (!obj || typeof obj !== "object") return false;
|
|
64
|
+
if (
|
|
65
|
+
"env" in obj ||
|
|
66
|
+
"client" in obj ||
|
|
67
|
+
"apiUrl" in obj ||
|
|
68
|
+
"timeoutMs" in obj ||
|
|
69
|
+
"minConfidence" in obj ||
|
|
70
|
+
"fetchImpl" in obj ||
|
|
71
|
+
"execFileImpl" in obj ||
|
|
72
|
+
"statsFile" in obj ||
|
|
73
|
+
"includeScore" in obj
|
|
74
|
+
) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return (
|
|
78
|
+
"TFX_JEV" in obj ||
|
|
79
|
+
"JEV_API_KEY" in obj ||
|
|
80
|
+
"TYPESAFE_API_KEY" in obj ||
|
|
81
|
+
"TFX_JEV_CLI" in obj ||
|
|
82
|
+
"TFX_JEV_STATS_FILE" in obj ||
|
|
83
|
+
"PATH" in obj
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveEnv(options = {}) {
|
|
88
|
+
if (!options || typeof options !== "object") return process.env;
|
|
89
|
+
if (isDirectEnvObject(options)) return options;
|
|
90
|
+
return options.env || process.env;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isJevEnabled(options = {}) {
|
|
94
|
+
const env = resolveEnv(options);
|
|
95
|
+
if (isExplicitlyDisabled(env)) return false;
|
|
96
|
+
if (
|
|
97
|
+
options &&
|
|
98
|
+
typeof options === "object" &&
|
|
99
|
+
!isDirectEnvObject(options) &&
|
|
100
|
+
(typeof options.client === "function" ||
|
|
101
|
+
typeof options.batchClient === "function")
|
|
102
|
+
) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (resolveJevApiKey(env)) return true;
|
|
106
|
+
if (resolveJevCliBin(env)) return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 프롬프트와 질문은 클램프 전에 정리하고 HTTP/CLI 공통 경계에서도 재검사한다.
|
|
111
|
+
function sanitizeJevInput(value) {
|
|
112
|
+
if (typeof value === "string") {
|
|
113
|
+
return value
|
|
114
|
+
.replace(
|
|
115
|
+
/-----BEGIN [^\r\n-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^\r\n-]*PRIVATE KEY-----|$)/gi,
|
|
116
|
+
"[REDACTED]",
|
|
117
|
+
)
|
|
118
|
+
.replace(
|
|
119
|
+
/\b(?:proxy-)?authorization["']?\s*[:=]\s*(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\r\n,;}]+)/gi,
|
|
120
|
+
"Authorization: [REDACTED]",
|
|
121
|
+
)
|
|
122
|
+
.replace(/\bBearer\s+[^\s"',;]+/gi, "Bearer [REDACTED]")
|
|
123
|
+
.replace(
|
|
124
|
+
/\b([\w.-]*(?:api[_-]?key|private[_-]?key|token|password|passwd|secret|credential)[\w.-]*["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;}\]]+)/gi,
|
|
125
|
+
"$1[REDACTED]",
|
|
126
|
+
)
|
|
127
|
+
.replace(
|
|
128
|
+
/\b(?:sk-(?:proj-|ant-)?[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{16,}|AIza[A-Za-z0-9_-]{20,})\b/g,
|
|
129
|
+
"[REDACTED]",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(value)) return value.map(sanitizeJevInput);
|
|
133
|
+
if (value && typeof value === "object") {
|
|
134
|
+
return Object.fromEntries(
|
|
135
|
+
Object.entries(value).map(([key, item]) => [
|
|
136
|
+
sanitizeJevInput(key),
|
|
137
|
+
/authorization|api[_-]?key|token|password|passwd|secret|credential|private[_-]?key/i.test(
|
|
138
|
+
key,
|
|
139
|
+
)
|
|
140
|
+
? "[REDACTED]"
|
|
141
|
+
: sanitizeJevInput(item),
|
|
142
|
+
]),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 워커/TUI 출력은 내용이나 해시 없이 로컬에서 계산한 상태 신호만 전송한다.
|
|
149
|
+
function outputMetadata(text) {
|
|
150
|
+
const value = String(text ?? "");
|
|
151
|
+
return JSON.stringify({
|
|
152
|
+
chars: value.length,
|
|
153
|
+
lines: value ? value.split(/\r?\n/).length : 0,
|
|
154
|
+
hasProgress:
|
|
155
|
+
/\b(?:running|working|thinking|compiling|testing|editing)\b|진행|작업 중/iu.test(
|
|
156
|
+
value,
|
|
157
|
+
),
|
|
158
|
+
waitingInput: /\[y\/n\]|\(y\/n\)|proceed\?|confirm|입력 대기/iu.test(value),
|
|
159
|
+
hasError: /\b(?:error|failed|fatal|exception)\b|오류|실패/iu.test(value),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function clampText(text, maxChars = 1600) {
|
|
164
|
+
const str = sanitizeJevInput(String(text ?? "")).trim();
|
|
165
|
+
if (str.length <= maxChars) return str;
|
|
166
|
+
return str.slice(-maxChars);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 두 의미 판정만 짧은 단일 문장을 허용한다. 로그에서 줄을 골라내지 않는다.
|
|
170
|
+
function semanticExcerpt(value, maxChars = 600) {
|
|
171
|
+
const text = String(value ?? "");
|
|
172
|
+
if (
|
|
173
|
+
!text.trim() ||
|
|
174
|
+
text.length > maxChars ||
|
|
175
|
+
/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(text) ||
|
|
176
|
+
/\b(?:set-cookie|cookie|(?:proxy-)?authorization)\b[\s"']*[:=]/i.test(
|
|
177
|
+
text,
|
|
178
|
+
) ||
|
|
179
|
+
/^\s*(?:[[{]|\d{4}-\d{2}-\d{2}[T ])/.test(text)
|
|
180
|
+
) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const protectedText = sanitizeJevInput(text);
|
|
184
|
+
// 보호나 자르기로 반대 의미가 없어질 수 있으므로 변경된 입력은 판정하지 않는다.
|
|
185
|
+
// 공백도 보존해 서로 다른 원문이 같은 발췌로 합쳐지지 않게 한다.
|
|
186
|
+
return protectedText === text ? protectedText : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 경계 라벨과 값이 결합된 뒤 보호 결과가 바뀌는 경우도 호출 전에 거절한다.
|
|
190
|
+
function semanticContext(excerpts) {
|
|
191
|
+
const context = JSON.stringify(excerpts);
|
|
192
|
+
return sanitizeJevInput(context) === context ? context : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isValidUnitNumber(val) {
|
|
196
|
+
return (
|
|
197
|
+
typeof val === "number" && Number.isFinite(val) && val >= 0 && val <= 1
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeResponse(rawInput, kind) {
|
|
202
|
+
if (!rawInput || typeof rawInput !== "object") return null;
|
|
203
|
+
|
|
204
|
+
const raw =
|
|
205
|
+
rawInput.answers && typeof rawInput.answers === "object"
|
|
206
|
+
? (rawInput.answers.q ?? Object.values(rawInput.answers)[0] ?? rawInput)
|
|
207
|
+
: rawInput;
|
|
208
|
+
if (!raw || typeof raw !== "object") return null;
|
|
209
|
+
|
|
210
|
+
// 공식 TypeSafe System One noul 응답: { noul: 0.98 } (0~1 사이 확률값 자체가 판정과 신뢰도를 겸함)
|
|
211
|
+
if (
|
|
212
|
+
kind === "noul" &&
|
|
213
|
+
isValidUnitNumber(raw.noul) &&
|
|
214
|
+
raw.answer === undefined &&
|
|
215
|
+
raw.value === undefined
|
|
216
|
+
) {
|
|
217
|
+
const p = raw.noul;
|
|
218
|
+
return {
|
|
219
|
+
value: p >= 0.5,
|
|
220
|
+
confidence: p >= 0.5 ? p : 1 - p,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let rawConf = raw.confidence ?? raw.probability ?? raw.prob;
|
|
225
|
+
if (
|
|
226
|
+
rawConf === undefined &&
|
|
227
|
+
kind === "choice" &&
|
|
228
|
+
typeof raw.choice === "string" &&
|
|
229
|
+
raw.probabilities &&
|
|
230
|
+
typeof raw.probabilities === "object"
|
|
231
|
+
) {
|
|
232
|
+
rawConf = raw.probabilities[raw.choice];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!isValidUnitNumber(rawConf)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const safeConfidence = rawConf;
|
|
239
|
+
|
|
240
|
+
if (kind === "noul") {
|
|
241
|
+
const val = raw.answer ?? raw.value ?? raw.result ?? raw.output ?? raw.yes;
|
|
242
|
+
if (typeof val === "boolean") {
|
|
243
|
+
return { value: val, confidence: safeConfidence };
|
|
244
|
+
}
|
|
245
|
+
if (typeof val === "string") {
|
|
246
|
+
const lowered = val.trim().toLowerCase();
|
|
247
|
+
if (lowered === "yes" || lowered === "true") {
|
|
248
|
+
return { value: true, confidence: safeConfidence };
|
|
249
|
+
}
|
|
250
|
+
if (lowered === "no" || lowered === "false") {
|
|
251
|
+
return { value: false, confidence: safeConfidence };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (kind === "choice") {
|
|
258
|
+
const choice =
|
|
259
|
+
raw.choice ?? raw.answer ?? raw.value ?? raw.result ?? raw.output;
|
|
260
|
+
if (typeof choice === "string" && choice.trim()) {
|
|
261
|
+
return { choice: choice.trim(), confidence: safeConfidence };
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (kind === "score") {
|
|
267
|
+
const score = Number(
|
|
268
|
+
raw.score ?? raw.value ?? raw.answer ?? raw.result ?? raw.output,
|
|
269
|
+
);
|
|
270
|
+
if (Number.isFinite(score)) {
|
|
271
|
+
return { score, confidence: safeConfidence };
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildSystemOneQuestion(request) {
|
|
280
|
+
if (request.kind === "choice") {
|
|
281
|
+
const criteria =
|
|
282
|
+
request.criteria && typeof request.criteria === "object"
|
|
283
|
+
? request.criteria
|
|
284
|
+
: Object.fromEntries(
|
|
285
|
+
(request.options || []).map((opt) => [String(opt), String(opt)]),
|
|
286
|
+
);
|
|
287
|
+
return {
|
|
288
|
+
type: "choice",
|
|
289
|
+
instructions: request.query,
|
|
290
|
+
criteria,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (request.kind === "score") {
|
|
294
|
+
return {
|
|
295
|
+
type: "score",
|
|
296
|
+
instructions: request.query,
|
|
297
|
+
...(request.criteria ? { criteria: request.criteria } : {}),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
type: "noul",
|
|
302
|
+
instructions: request.query,
|
|
303
|
+
...(request.criteria ? { criteria: request.criteria } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function callJevHttp(request, options = {}) {
|
|
308
|
+
const env = resolveEnv(options);
|
|
309
|
+
const apiKey = resolveJevApiKey(env);
|
|
310
|
+
if (!apiKey) return null;
|
|
311
|
+
|
|
312
|
+
const url = options.apiUrl || env.JEV_API_URL || DEFAULT_JEV_API_URL;
|
|
313
|
+
const model = options.model || env.JEV_MODEL || DEFAULT_JEV_MODEL;
|
|
314
|
+
const timeoutMs = Number(
|
|
315
|
+
options.timeoutMs ?? env.TFX_JEV_TIMEOUT_MS ?? DEFAULT_JEV_TIMEOUT_MS,
|
|
316
|
+
);
|
|
317
|
+
const fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
318
|
+
if (typeof fetchImpl !== "function") return null;
|
|
319
|
+
|
|
320
|
+
const payload = {
|
|
321
|
+
model,
|
|
322
|
+
state: request.context
|
|
323
|
+
? `${request.context}\n\nQuestion: ${request.query}`
|
|
324
|
+
: String(request.query || ""),
|
|
325
|
+
questions: {
|
|
326
|
+
q: buildSystemOneQuestion(request),
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const response = await fetchImpl(url, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: {
|
|
333
|
+
"Content-Type": "application/json",
|
|
334
|
+
Authorization: `Bearer ${apiKey}`,
|
|
335
|
+
},
|
|
336
|
+
body: JSON.stringify(sanitizeJevInput(payload)),
|
|
337
|
+
signal: AbortSignal.timeout(timeoutMs > 0 ? timeoutMs : 1500),
|
|
338
|
+
});
|
|
339
|
+
if (!response.ok) return null;
|
|
340
|
+
const data = await response.json();
|
|
341
|
+
return normalizeResponse(data, request.kind);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function callJevCli(request, options = {}) {
|
|
345
|
+
request = sanitizeJevInput(request);
|
|
346
|
+
const env = resolveEnv(options);
|
|
347
|
+
const cliBin = resolveJevCliBin(env);
|
|
348
|
+
if (!cliBin) return null;
|
|
349
|
+
|
|
350
|
+
const timeoutMs = Number(
|
|
351
|
+
options.timeoutMs ?? env.TFX_JEV_TIMEOUT_MS ?? DEFAULT_JEV_TIMEOUT_MS,
|
|
352
|
+
);
|
|
353
|
+
const args = [request.kind, request.query, "--json"];
|
|
354
|
+
if (request.context) {
|
|
355
|
+
args.push("--context", request.context);
|
|
356
|
+
}
|
|
357
|
+
if (Array.isArray(request.options) && request.options.length > 0) {
|
|
358
|
+
args.push("--options", request.options.join(","));
|
|
359
|
+
}
|
|
360
|
+
const execImpl = options.execFileImpl || execFileAsync;
|
|
361
|
+
const { stdout } = await execImpl(cliBin, args, {
|
|
362
|
+
timeout: timeoutMs > 0 ? timeoutMs : 1500,
|
|
363
|
+
maxBuffer: 256 * 1024,
|
|
364
|
+
env: env === process.env ? process.env : { ...process.env, ...env },
|
|
365
|
+
});
|
|
366
|
+
const parsed = JSON.parse(String(stdout ?? "").trim());
|
|
367
|
+
return normalizeResponse(parsed, request.kind);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function queryJevBatch(input = {}, options = {}) {
|
|
371
|
+
if (!isJevEnabled(options)) return null;
|
|
372
|
+
if (Array.isArray(input)) {
|
|
373
|
+
const questions = {};
|
|
374
|
+
const contexts = [];
|
|
375
|
+
input.forEach((item, idx) => {
|
|
376
|
+
const key = `q_${idx}`;
|
|
377
|
+
questions[key] = item;
|
|
378
|
+
if (item.context) contexts.push(item.context);
|
|
379
|
+
});
|
|
380
|
+
const objRes = await queryJevBatch(
|
|
381
|
+
{ state: contexts[0] || "", questions },
|
|
382
|
+
options,
|
|
383
|
+
);
|
|
384
|
+
if (!objRes) return null;
|
|
385
|
+
return input.map((_, idx) => objRes[`q_${idx}`] || null);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const { state = "", questions = {} } = sanitizeJevInput(input);
|
|
389
|
+
const entries = Object.entries(questions || {});
|
|
390
|
+
const startedAt = Date.now();
|
|
391
|
+
const fallback = (reason) => {
|
|
392
|
+
recordJevStat(
|
|
393
|
+
{
|
|
394
|
+
hook: "jev_batch",
|
|
395
|
+
kind: "batch",
|
|
396
|
+
intervened: false,
|
|
397
|
+
reason,
|
|
398
|
+
durationMs: Date.now() - startedAt,
|
|
399
|
+
},
|
|
400
|
+
options,
|
|
401
|
+
);
|
|
402
|
+
return null;
|
|
403
|
+
};
|
|
404
|
+
if (entries.length === 0) return fallback("batch_empty_questions");
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const clientFn = options.batchClient || options.client;
|
|
408
|
+
if (typeof clientFn === "function") {
|
|
409
|
+
const custom = await clientFn({ state, questions }, options);
|
|
410
|
+
const answers = custom?.answers || custom || {};
|
|
411
|
+
const out = {};
|
|
412
|
+
for (const [key, req] of entries) {
|
|
413
|
+
out[key] = normalizeResponse(answers[key], req.kind);
|
|
414
|
+
}
|
|
415
|
+
if (Object.values(out).some((answer) => !answer))
|
|
416
|
+
fallback("batch_invalid_response");
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const env = resolveEnv(options);
|
|
421
|
+
const apiKey = resolveJevApiKey(env);
|
|
422
|
+
if (!apiKey) {
|
|
423
|
+
if (!resolveJevCliBin(env))
|
|
424
|
+
return fallback("batch_transport_unavailable");
|
|
425
|
+
const out = {};
|
|
426
|
+
for (const [key, req] of entries) {
|
|
427
|
+
out[key] = await callJevCli(
|
|
428
|
+
{
|
|
429
|
+
...req,
|
|
430
|
+
context: [state, req.context].filter(Boolean).join("\n\n"),
|
|
431
|
+
},
|
|
432
|
+
options,
|
|
433
|
+
);
|
|
434
|
+
if (!out[key]) return fallback("batch_invalid_response");
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const url = options.apiUrl || env.JEV_API_URL || DEFAULT_JEV_API_URL;
|
|
440
|
+
const model = options.model || env.JEV_MODEL || DEFAULT_JEV_MODEL;
|
|
441
|
+
const timeoutMs = Number(
|
|
442
|
+
options.timeoutMs ?? env.TFX_JEV_TIMEOUT_MS ?? DEFAULT_JEV_TIMEOUT_MS,
|
|
443
|
+
);
|
|
444
|
+
const fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
445
|
+
if (typeof fetchImpl !== "function")
|
|
446
|
+
return fallback("batch_transport_unavailable");
|
|
447
|
+
|
|
448
|
+
const payloadQuestions = {};
|
|
449
|
+
for (const [key, req] of entries) {
|
|
450
|
+
payloadQuestions[key] = buildSystemOneQuestion(req);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const response = await fetchImpl(url, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
headers: {
|
|
456
|
+
"Content-Type": "application/json",
|
|
457
|
+
Authorization: `Bearer ${apiKey}`,
|
|
458
|
+
},
|
|
459
|
+
body: JSON.stringify({
|
|
460
|
+
model,
|
|
461
|
+
state: String(state || ""),
|
|
462
|
+
questions: payloadQuestions,
|
|
463
|
+
}),
|
|
464
|
+
signal: AbortSignal.timeout(timeoutMs > 0 ? timeoutMs : 1500),
|
|
465
|
+
});
|
|
466
|
+
if (!response.ok) return fallback("batch_http_failed");
|
|
467
|
+
const data = await response.json();
|
|
468
|
+
const answers = data?.answers || {};
|
|
469
|
+
const out = {};
|
|
470
|
+
for (const [key, req] of entries) {
|
|
471
|
+
out[key] = normalizeResponse(answers[key], req.kind);
|
|
472
|
+
}
|
|
473
|
+
if (Object.values(out).some((answer) => !answer))
|
|
474
|
+
fallback("batch_invalid_response");
|
|
475
|
+
return out;
|
|
476
|
+
} catch {
|
|
477
|
+
return fallback("batch_call_failed");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function recordJevStat(entry = {}, options = {}) {
|
|
482
|
+
try {
|
|
483
|
+
const env = resolveEnv(options);
|
|
484
|
+
const flag = String(env.TFX_JEV_STATS ?? "")
|
|
485
|
+
.trim()
|
|
486
|
+
.toLowerCase();
|
|
487
|
+
if (flag === "0" || flag === "false" || flag === "off") return;
|
|
488
|
+
|
|
489
|
+
const explicitFile = options.statsFile || env.TFX_JEV_STATS_FILE || "";
|
|
490
|
+
if (!explicitFile && typeof options.client === "function") return;
|
|
491
|
+
|
|
492
|
+
const filePath = explicitFile || DEFAULT_JEV_STATS_PATH;
|
|
493
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
494
|
+
appendFileSync(
|
|
495
|
+
filePath,
|
|
496
|
+
`${JSON.stringify({
|
|
497
|
+
ts: new Date().toISOString(),
|
|
498
|
+
...entry,
|
|
499
|
+
})}\n`,
|
|
500
|
+
"utf8",
|
|
501
|
+
);
|
|
502
|
+
} catch {
|
|
503
|
+
// 통계 기록 실패는 무시한다.
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function summarizeJevStats(options = {}) {
|
|
508
|
+
const env = resolveEnv(options);
|
|
509
|
+
const filePath =
|
|
510
|
+
options.statsFile || env.TFX_JEV_STATS_FILE || DEFAULT_JEV_STATS_PATH;
|
|
511
|
+
const summary = {
|
|
512
|
+
path: filePath,
|
|
513
|
+
exists: false,
|
|
514
|
+
total: 0,
|
|
515
|
+
interventions: 0,
|
|
516
|
+
fallbacks: 0,
|
|
517
|
+
avgMs: 0,
|
|
518
|
+
byHook: {},
|
|
519
|
+
};
|
|
520
|
+
if (!existsSync(filePath)) return summary;
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const lines = readFileSync(filePath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
524
|
+
summary.exists = true;
|
|
525
|
+
let totalMs = 0;
|
|
526
|
+
let msCount = 0;
|
|
527
|
+
for (const line of lines) {
|
|
528
|
+
let row;
|
|
529
|
+
try {
|
|
530
|
+
row = JSON.parse(line);
|
|
531
|
+
} catch {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
summary.total += 1;
|
|
535
|
+
const hook = String(row.hook || "unknown");
|
|
536
|
+
if (!summary.byHook[hook]) {
|
|
537
|
+
summary.byHook[hook] = { total: 0, interventions: 0, fallbacks: 0 };
|
|
538
|
+
}
|
|
539
|
+
summary.byHook[hook].total += 1;
|
|
540
|
+
if (row.intervened) {
|
|
541
|
+
summary.interventions += 1;
|
|
542
|
+
summary.byHook[hook].interventions += 1;
|
|
543
|
+
} else {
|
|
544
|
+
summary.fallbacks += 1;
|
|
545
|
+
summary.byHook[hook].fallbacks += 1;
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
typeof row.durationMs === "number" &&
|
|
549
|
+
Number.isFinite(row.durationMs)
|
|
550
|
+
) {
|
|
551
|
+
totalMs += row.durationMs;
|
|
552
|
+
msCount += 1;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
summary.avgMs = msCount > 0 ? Math.round(totalMs / msCount) : 0;
|
|
556
|
+
} catch {
|
|
557
|
+
// 읽기 실패 시 빈 요약 반환
|
|
558
|
+
}
|
|
559
|
+
return summary;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export async function queryJev(request, options = {}) {
|
|
563
|
+
if (!isJevEnabled(options)) return null;
|
|
564
|
+
request = sanitizeJevInput(request);
|
|
565
|
+
const startedAt = Date.now();
|
|
566
|
+
try {
|
|
567
|
+
let result = null;
|
|
568
|
+
if (typeof options.client === "function") {
|
|
569
|
+
const custom = await options.client(request, options);
|
|
570
|
+
result = normalizeResponse(custom, request.kind);
|
|
571
|
+
} else {
|
|
572
|
+
const env = resolveEnv(options);
|
|
573
|
+
if (resolveJevApiKey(env)) {
|
|
574
|
+
result = await callJevHttp(request, options);
|
|
575
|
+
} else if (resolveJevCliBin(env)) {
|
|
576
|
+
result = await callJevCli(request, options);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (request.hook) {
|
|
580
|
+
const threshold = minConfidence(options);
|
|
581
|
+
const intervened = Boolean(result && result.confidence >= threshold);
|
|
582
|
+
recordJevStat(
|
|
583
|
+
{
|
|
584
|
+
hook: request.hook,
|
|
585
|
+
kind: request.kind,
|
|
586
|
+
intervened,
|
|
587
|
+
confidence: result?.confidence ?? null,
|
|
588
|
+
outcome:
|
|
589
|
+
result?.choice ??
|
|
590
|
+
(typeof result?.value === "boolean" ? result.value : null) ??
|
|
591
|
+
result?.score ??
|
|
592
|
+
null,
|
|
593
|
+
durationMs: Date.now() - startedAt,
|
|
594
|
+
},
|
|
595
|
+
options,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return result;
|
|
599
|
+
} catch {
|
|
600
|
+
if (request.hook) {
|
|
601
|
+
recordJevStat(
|
|
602
|
+
{
|
|
603
|
+
hook: request.hook,
|
|
604
|
+
kind: request.kind,
|
|
605
|
+
intervened: false,
|
|
606
|
+
confidence: null,
|
|
607
|
+
outcome: null,
|
|
608
|
+
durationMs: Date.now() - startedAt,
|
|
609
|
+
},
|
|
610
|
+
options,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function minConfidence(options = {}) {
|
|
618
|
+
const env = resolveEnv(options);
|
|
619
|
+
const threshold = Number(
|
|
620
|
+
options.minConfidence ??
|
|
621
|
+
env.TFX_JEV_MIN_CONFIDENCE ??
|
|
622
|
+
DEFAULT_JEV_CONFIDENCE_THRESHOLD,
|
|
623
|
+
);
|
|
624
|
+
return Number.isFinite(threshold)
|
|
625
|
+
? threshold
|
|
626
|
+
: DEFAULT_JEV_CONFIDENCE_THRESHOLD;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* 1. 재시도 상태 머신: 두 실패 메시지가 동일한 근본 원인인지 판정한다.
|
|
631
|
+
*/
|
|
632
|
+
export async function judgeSameFailureCause(
|
|
633
|
+
previousReason,
|
|
634
|
+
currentReason,
|
|
635
|
+
options = {},
|
|
636
|
+
) {
|
|
637
|
+
if (!isJevEnabled(options)) return null;
|
|
638
|
+
const prev = semanticExcerpt(previousReason);
|
|
639
|
+
const curr = semanticExcerpt(currentReason);
|
|
640
|
+
if (prev === null || curr === null) return null;
|
|
641
|
+
const context = semanticContext({
|
|
642
|
+
previousFailureExcerpt: prev,
|
|
643
|
+
currentFailureExcerpt: curr,
|
|
644
|
+
});
|
|
645
|
+
if (context === null) return null;
|
|
646
|
+
if (prev === curr) return { sameCause: true, confidence: 1 };
|
|
647
|
+
|
|
648
|
+
const res = await queryJev(
|
|
649
|
+
{
|
|
650
|
+
hook: "same_failure_cause",
|
|
651
|
+
kind: "noul",
|
|
652
|
+
query:
|
|
653
|
+
"Are both failure messages caused by the exact same underlying blocker or root error (ignoring timestamps, PIDs, ports, or line number shifts)?",
|
|
654
|
+
context,
|
|
655
|
+
},
|
|
656
|
+
options,
|
|
657
|
+
);
|
|
658
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
659
|
+
return { sameCause: res.value, confidence: res.confidence };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* 2. 워커 인계 파일(handoff) 보정 및 가짜 성공(exit 0) 필터링.
|
|
664
|
+
*/
|
|
665
|
+
export async function judgeHandoffQuality(
|
|
666
|
+
{
|
|
667
|
+
rawText = "",
|
|
668
|
+
handoff = {},
|
|
669
|
+
exitCode = 0,
|
|
670
|
+
gitDiffFiles = [],
|
|
671
|
+
role = "",
|
|
672
|
+
} = {},
|
|
673
|
+
options = {},
|
|
674
|
+
) {
|
|
675
|
+
if (!isJevEnabled(options)) return null;
|
|
676
|
+
if (!String(rawText).trim() && !handoff?.verdict) return null;
|
|
677
|
+
|
|
678
|
+
const allowed = ["accept", "needs_read", "retry"];
|
|
679
|
+
const filesCount = Array.isArray(gitDiffFiles)
|
|
680
|
+
? gitDiffFiles.filter(Boolean).length
|
|
681
|
+
: 0;
|
|
682
|
+
const res = await queryJev(
|
|
683
|
+
{
|
|
684
|
+
hook: "handoff_quality",
|
|
685
|
+
kind: "choice",
|
|
686
|
+
options: allowed,
|
|
687
|
+
criteria: {
|
|
688
|
+
accept:
|
|
689
|
+
"The assigned code implementation and verification were completed cleanly with no skipped checks.",
|
|
690
|
+
needs_read:
|
|
691
|
+
"The output is an analysis report, or tests/verification were skipped or left ambiguous so the lead must read the details.",
|
|
692
|
+
retry:
|
|
693
|
+
"The worker exited without completing the implementation or hit an unresolved error that requires a retry.",
|
|
694
|
+
},
|
|
695
|
+
query:
|
|
696
|
+
"Classify the required lead action for this worker output: 'accept' if the assigned task was completed cleanly, 'needs_read' if it is an analysis/report or ambiguous partial result that the lead must read, or 'retry' if the worker exited without completing the task or hit an unresolved error.",
|
|
697
|
+
context: [
|
|
698
|
+
`exit_code: ${exitCode}`,
|
|
699
|
+
`role: ${role || "worker"}`,
|
|
700
|
+
`declared_task_metadata: ${outputMetadata(handoff?.task)}`,
|
|
701
|
+
`files_changed_count: ${filesCount}`,
|
|
702
|
+
`declared_status: ${outputMetadata(handoff?.status)}`,
|
|
703
|
+
`declared_action: ${outputMetadata(handoff?.lead_action)}`,
|
|
704
|
+
`declared_verdict: ${outputMetadata(handoff?.verdict)}`,
|
|
705
|
+
`output_metadata: ${outputMetadata(rawText)}`,
|
|
706
|
+
].join("\n"),
|
|
707
|
+
},
|
|
708
|
+
options,
|
|
709
|
+
);
|
|
710
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
711
|
+
if (!allowed.includes(res.choice)) return null;
|
|
712
|
+
return { leadAction: res.choice, confidence: res.confidence };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* 3. 피어(peer) 토론 및 교차 리뷰 조기 종료 판정.
|
|
717
|
+
*/
|
|
718
|
+
export async function judgePeerConvergence(
|
|
719
|
+
{
|
|
720
|
+
seed = "",
|
|
721
|
+
mode = "debate",
|
|
722
|
+
previousResponse = "",
|
|
723
|
+
currentResponse = "",
|
|
724
|
+
hopIndex = 0,
|
|
725
|
+
participantsCount = 2,
|
|
726
|
+
} = {},
|
|
727
|
+
options = {},
|
|
728
|
+
) {
|
|
729
|
+
if (!isJevEnabled(options)) return null;
|
|
730
|
+
if (mode === "counting") return null;
|
|
731
|
+
if (hopIndex < Math.max(2, participantsCount)) return null;
|
|
732
|
+
|
|
733
|
+
const prev = semanticExcerpt(previousResponse);
|
|
734
|
+
const curr = semanticExcerpt(currentResponse);
|
|
735
|
+
const task = seed ? semanticExcerpt(seed, 500) : "";
|
|
736
|
+
if (prev === null || curr === null || task === null) return null;
|
|
737
|
+
const context = semanticContext({
|
|
738
|
+
...(task ? { task } : {}),
|
|
739
|
+
previousTurnExcerpt: prev,
|
|
740
|
+
currentTurnExcerpt: curr,
|
|
741
|
+
});
|
|
742
|
+
if (context === null) return null;
|
|
743
|
+
|
|
744
|
+
const res = await queryJev(
|
|
745
|
+
{
|
|
746
|
+
hook: "peer_convergence",
|
|
747
|
+
kind: "noul",
|
|
748
|
+
query:
|
|
749
|
+
"Have the participants reached substantive agreement on the core task or entered a repetitive nitpick loop where further rounds will not add new conclusions?",
|
|
750
|
+
context,
|
|
751
|
+
},
|
|
752
|
+
options,
|
|
753
|
+
);
|
|
754
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
755
|
+
return { converged: res.value, confidence: res.confidence };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* 4. 작업 중인 워커에 대한 --if-busy auto (steer vs wait) 판단.
|
|
760
|
+
*/
|
|
761
|
+
export async function judgeBusyAction(
|
|
762
|
+
{
|
|
763
|
+
prompt = "",
|
|
764
|
+
incomingMessage = "",
|
|
765
|
+
threadSummary = "",
|
|
766
|
+
currentOutput = "",
|
|
767
|
+
allowDrop = false,
|
|
768
|
+
} = {},
|
|
769
|
+
options = {},
|
|
770
|
+
) {
|
|
771
|
+
if (!isJevEnabled(options)) return null;
|
|
772
|
+
const resolvedPrompt = prompt || incomingMessage;
|
|
773
|
+
const resolvedSummary = threadSummary || currentOutput;
|
|
774
|
+
const trimmed = clampText(resolvedPrompt, 1000);
|
|
775
|
+
if (!trimmed) return null;
|
|
776
|
+
|
|
777
|
+
const includeDrop =
|
|
778
|
+
allowDrop === true || Boolean(incomingMessage || currentOutput);
|
|
779
|
+
const allowed = includeDrop ? ["steer", "wait", "drop"] : ["steer", "wait"];
|
|
780
|
+
const criteria = {
|
|
781
|
+
steer:
|
|
782
|
+
"Urgent course correction, constraint change, or abort instruction that must interrupt the active turn immediately.",
|
|
783
|
+
wait: "Sequential follow-up task or next step that should wait in queue until the active turn finishes.",
|
|
784
|
+
...(includeDrop
|
|
785
|
+
? {
|
|
786
|
+
drop: "Duplicate or redundant instruction already present in the active output that should be skipped.",
|
|
787
|
+
}
|
|
788
|
+
: {}),
|
|
789
|
+
};
|
|
790
|
+
const res = await queryJev(
|
|
791
|
+
{
|
|
792
|
+
hook: "busy_action",
|
|
793
|
+
kind: "choice",
|
|
794
|
+
options: allowed,
|
|
795
|
+
criteria,
|
|
796
|
+
query: includeDrop
|
|
797
|
+
? "The worker is currently busy executing a turn. Choose 'steer' if this message corrects course or aborts current work immediately, 'wait' if it is a sequential follow-up task, or 'drop' if it duplicates work already running."
|
|
798
|
+
: "The worker is currently busy executing a turn. Choose 'steer' if this message corrects course, changes constraints, or aborts current work immediately; choose 'wait' if this message is a sequential follow-up task to run after the current turn finishes.",
|
|
799
|
+
context: [
|
|
800
|
+
resolvedSummary
|
|
801
|
+
? `Active thread metadata: ${outputMetadata(resolvedSummary)}`
|
|
802
|
+
: "",
|
|
803
|
+
`Incoming message:\n${trimmed}`,
|
|
804
|
+
]
|
|
805
|
+
.filter(Boolean)
|
|
806
|
+
.join("\n\n"),
|
|
807
|
+
},
|
|
808
|
+
options,
|
|
809
|
+
);
|
|
810
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
811
|
+
if (!allowed.includes(res.choice)) return null;
|
|
812
|
+
return { action: res.choice, confidence: res.confidence };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 5. 하트비트 모니터: 로그 꼬리 기반 의미상 먹통(프롬프트 대기, 에러 무한 루프) 판정.
|
|
817
|
+
*/
|
|
818
|
+
export async function judgeWorkerHealth(
|
|
819
|
+
{
|
|
820
|
+
stdoutTail = "",
|
|
821
|
+
stderrTail = "",
|
|
822
|
+
rolloutTail = "",
|
|
823
|
+
recentOutput = "",
|
|
824
|
+
elapsedSec = 0,
|
|
825
|
+
} = {},
|
|
826
|
+
options = {},
|
|
827
|
+
) {
|
|
828
|
+
if (!isJevEnabled(options)) return null;
|
|
829
|
+
const resolvedStdoutTail = stdoutTail || recentOutput;
|
|
830
|
+
const combined = [
|
|
831
|
+
resolvedStdoutTail
|
|
832
|
+
? `stdout_metadata: ${outputMetadata(resolvedStdoutTail)}`
|
|
833
|
+
: "",
|
|
834
|
+
stderrTail ? `stderr_metadata: ${outputMetadata(stderrTail)}` : "",
|
|
835
|
+
rolloutTail ? `rollout_metadata: ${outputMetadata(rolloutTail)}` : "",
|
|
836
|
+
]
|
|
837
|
+
.filter(Boolean)
|
|
838
|
+
.join("\n\n");
|
|
839
|
+
if (!combined) return null;
|
|
840
|
+
|
|
841
|
+
const allowed = ["progressing", "waiting_input", "looping_error"];
|
|
842
|
+
const res = await queryJev(
|
|
843
|
+
{
|
|
844
|
+
hook: "worker_health",
|
|
845
|
+
kind: "choice",
|
|
846
|
+
options: allowed,
|
|
847
|
+
criteria: {
|
|
848
|
+
progressing:
|
|
849
|
+
"Normal work, compilation, testing, or file editing is actively progressing.",
|
|
850
|
+
waiting_input:
|
|
851
|
+
"Blocked waiting for an interactive user input, y/n confirmation, or terminal prompt.",
|
|
852
|
+
looping_error:
|
|
853
|
+
"Stuck repeating the same fatal error or retry loop without progress.",
|
|
854
|
+
},
|
|
855
|
+
query:
|
|
856
|
+
"Inspect the worker's recent logs. Choose 'progressing' if normal work is happening, 'waiting_input' if blocked on an interactive y/n or confirmation prompt, or 'looping_error' if repeating the same fatal error in a loop.",
|
|
857
|
+
context: `elapsed_sec: ${elapsedSec}\n\n${combined}`,
|
|
858
|
+
},
|
|
859
|
+
options,
|
|
860
|
+
);
|
|
861
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
862
|
+
if (!allowed.includes(res.choice)) return null;
|
|
863
|
+
return {
|
|
864
|
+
status: res.choice,
|
|
865
|
+
healthState: res.choice,
|
|
866
|
+
confidence: res.confidence,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* 실행 모드별 CLI 권한 플래그 매핑 헬퍼.
|
|
872
|
+
* @param {"codex"|"antigravity"|"agy"|"claude"} cliType
|
|
873
|
+
* @param {"read_only"|"workspace_auto"|"bypass"} executionMode
|
|
874
|
+
*/
|
|
875
|
+
export function executionFlagsForCli(
|
|
876
|
+
cliType = "codex",
|
|
877
|
+
executionMode = "bypass",
|
|
878
|
+
) {
|
|
879
|
+
const normalizedCli = String(cliType || "codex")
|
|
880
|
+
.trim()
|
|
881
|
+
.toLowerCase();
|
|
882
|
+
const mode = ["read_only", "workspace_auto", "bypass"].includes(executionMode)
|
|
883
|
+
? executionMode
|
|
884
|
+
: "bypass";
|
|
885
|
+
|
|
886
|
+
if (normalizedCli === "antigravity" || normalizedCli === "agy") {
|
|
887
|
+
if (mode === "read_only" || mode === "workspace_auto") {
|
|
888
|
+
return {
|
|
889
|
+
cliType: "antigravity",
|
|
890
|
+
executionMode: mode,
|
|
891
|
+
args: ["--print", "--sandbox"],
|
|
892
|
+
flagsString: "--print --sandbox",
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
cliType: "antigravity",
|
|
897
|
+
executionMode: "bypass",
|
|
898
|
+
args: ["--print", "--dangerously-skip-permissions"],
|
|
899
|
+
flagsString: "--print --dangerously-skip-permissions",
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (normalizedCli === "claude") {
|
|
904
|
+
if (mode === "read_only") {
|
|
905
|
+
return {
|
|
906
|
+
cliType: "claude",
|
|
907
|
+
executionMode: "read_only",
|
|
908
|
+
args: ["--permission-mode", "plan"],
|
|
909
|
+
flagsString: "--permission-mode plan",
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (mode === "workspace_auto") {
|
|
913
|
+
return {
|
|
914
|
+
cliType: "claude",
|
|
915
|
+
executionMode: "workspace_auto",
|
|
916
|
+
args: ["--permission-mode", "acceptEdits"],
|
|
917
|
+
flagsString: "--permission-mode acceptEdits",
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
cliType: "claude",
|
|
922
|
+
executionMode: "bypass",
|
|
923
|
+
args: [
|
|
924
|
+
"--permission-mode",
|
|
925
|
+
"bypassPermissions",
|
|
926
|
+
"--allow-dangerously-skip-permissions",
|
|
927
|
+
],
|
|
928
|
+
flagsString:
|
|
929
|
+
"--permission-mode bypassPermissions --allow-dangerously-skip-permissions",
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (mode === "read_only") {
|
|
934
|
+
return {
|
|
935
|
+
cliType: "codex",
|
|
936
|
+
executionMode: "read_only",
|
|
937
|
+
args: ["--sandbox", "read-only", "--skip-git-repo-check"],
|
|
938
|
+
flagsString: "--sandbox read-only --skip-git-repo-check",
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (mode === "workspace_auto") {
|
|
942
|
+
return {
|
|
943
|
+
cliType: "codex",
|
|
944
|
+
executionMode: "workspace_auto",
|
|
945
|
+
args: ["--sandbox", "workspace-write", "--skip-git-repo-check"],
|
|
946
|
+
flagsString: "--sandbox workspace-write --skip-git-repo-check",
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
cliType: "codex",
|
|
951
|
+
executionMode: "bypass",
|
|
952
|
+
args: [
|
|
953
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
954
|
+
"--skip-git-repo-check",
|
|
955
|
+
],
|
|
956
|
+
flagsString:
|
|
957
|
+
"--dangerously-bypass-approvals-and-sandbox --skip-git-repo-check",
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const COMPLEXITY_ROUTE_MAP = Object.freeze({
|
|
962
|
+
trivial: Object.freeze({
|
|
963
|
+
codexProfile: "luna_low",
|
|
964
|
+
agyProfile: "argon_low",
|
|
965
|
+
claudeModel: "haiku",
|
|
966
|
+
claudeEffort: "low",
|
|
967
|
+
expectedDurationSec: 60,
|
|
968
|
+
}),
|
|
969
|
+
standard: Object.freeze({
|
|
970
|
+
codexProfile: "sol_high",
|
|
971
|
+
agyProfile: "argon",
|
|
972
|
+
claudeModel: "sonnet",
|
|
973
|
+
claudeEffort: "medium",
|
|
974
|
+
expectedDurationSec: 300,
|
|
975
|
+
}),
|
|
976
|
+
deep: Object.freeze({
|
|
977
|
+
codexProfile: "astra_xhigh",
|
|
978
|
+
agyProfile: "argon_high",
|
|
979
|
+
claudeModel: "opus",
|
|
980
|
+
claudeEffort: "high",
|
|
981
|
+
expectedDurationSec: 600,
|
|
982
|
+
}),
|
|
983
|
+
high_risk: Object.freeze({
|
|
984
|
+
codexProfile: "astra_xhigh",
|
|
985
|
+
agyProfile: "argon_high",
|
|
986
|
+
claudeModel: "opus",
|
|
987
|
+
claudeEffort: "high",
|
|
988
|
+
expectedDurationSec: 900,
|
|
989
|
+
}),
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* 6. Pre-route 통합 판단: 프롬프트 복잡도(모델 및 effort), 실행 권한 모드(read_only / workspace_auto / bypass),
|
|
994
|
+
* 필요한 MCP 프로필 및 서버 목록을 단일 queryJevBatch 호출로 판정한다.
|
|
995
|
+
*/
|
|
996
|
+
export async function judgeTaskRouting(
|
|
997
|
+
{
|
|
998
|
+
prompt = "",
|
|
999
|
+
agentType = "executor",
|
|
1000
|
+
cliType = "codex",
|
|
1001
|
+
availableServers = [],
|
|
1002
|
+
} = {},
|
|
1003
|
+
options = {},
|
|
1004
|
+
) {
|
|
1005
|
+
if (!isJevEnabled(options)) return null;
|
|
1006
|
+
const trimmedPrompt = clampText(prompt, 1400);
|
|
1007
|
+
if (!trimmedPrompt) return null;
|
|
1008
|
+
|
|
1009
|
+
const complexityOptions = ["trivial", "standard", "deep", "high_risk"];
|
|
1010
|
+
const executionModes = ["read_only", "workspace_auto", "bypass"];
|
|
1011
|
+
const mcpProfiles = [
|
|
1012
|
+
"none",
|
|
1013
|
+
"executor",
|
|
1014
|
+
"implementer",
|
|
1015
|
+
"designer",
|
|
1016
|
+
"analyze",
|
|
1017
|
+
"explore",
|
|
1018
|
+
"reviewer",
|
|
1019
|
+
"writer",
|
|
1020
|
+
"default",
|
|
1021
|
+
];
|
|
1022
|
+
const candidateServers = Array.isArray(availableServers)
|
|
1023
|
+
? availableServers
|
|
1024
|
+
.map((s) => String(s ?? "").trim())
|
|
1025
|
+
.filter(Boolean)
|
|
1026
|
+
.slice(0, 12)
|
|
1027
|
+
: [];
|
|
1028
|
+
|
|
1029
|
+
const contextBlock = [
|
|
1030
|
+
`agent_role: ${agentType || "executor"}`,
|
|
1031
|
+
`cli_type: ${cliType || "codex"}`,
|
|
1032
|
+
candidateServers.length > 0
|
|
1033
|
+
? `available_mcp_servers: ${candidateServers.join(", ")}`
|
|
1034
|
+
: "",
|
|
1035
|
+
`task_prompt:\n${trimmedPrompt}`,
|
|
1036
|
+
]
|
|
1037
|
+
.filter(Boolean)
|
|
1038
|
+
.join("\n");
|
|
1039
|
+
|
|
1040
|
+
const questions = {
|
|
1041
|
+
complexity: {
|
|
1042
|
+
hook: "task_routing_complexity",
|
|
1043
|
+
kind: "choice",
|
|
1044
|
+
options: complexityOptions,
|
|
1045
|
+
criteria: {
|
|
1046
|
+
trivial:
|
|
1047
|
+
"Single-line lookup, formatting, typo fix, simple status check, or short question.",
|
|
1048
|
+
standard:
|
|
1049
|
+
"Routine feature implementation, unit test writing, bug fix, or single-module refactoring.",
|
|
1050
|
+
deep: "Multi-file architecture redesign, root-cause debugging across modules, or complex consensus synthesis.",
|
|
1051
|
+
high_risk:
|
|
1052
|
+
"Security-critical change, release/deployment automation, database migration, or destructive system operation.",
|
|
1053
|
+
},
|
|
1054
|
+
query:
|
|
1055
|
+
"Classify the complexity and reasoning depth required for this task.",
|
|
1056
|
+
},
|
|
1057
|
+
execution_mode: {
|
|
1058
|
+
hook: "task_routing_execution_mode",
|
|
1059
|
+
kind: "choice",
|
|
1060
|
+
options: executionModes,
|
|
1061
|
+
criteria: {
|
|
1062
|
+
read_only:
|
|
1063
|
+
"Only reading, searching, reviewing, or explaining code and docs without modifying files or running mutating commands.",
|
|
1064
|
+
workspace_auto:
|
|
1065
|
+
"Modifying files inside the repository and running local unit tests or build scripts without external network, git push, ssh, or system-wide state changes.",
|
|
1066
|
+
bypass:
|
|
1067
|
+
"Requires git push/gh CLI, ssh, external network requests, package installation, daemon/socket management, or writing outside the workspace.",
|
|
1068
|
+
},
|
|
1069
|
+
query:
|
|
1070
|
+
"Choose the minimum execution permission mode needed to complete this task safely.",
|
|
1071
|
+
},
|
|
1072
|
+
mcp_profile: {
|
|
1073
|
+
hook: "task_routing_mcp_profile",
|
|
1074
|
+
kind: "choice",
|
|
1075
|
+
options: mcpProfiles,
|
|
1076
|
+
criteria: {
|
|
1077
|
+
none: "Pure local code editing, local shell execution, or local unit test run that needs no external MCP servers.",
|
|
1078
|
+
executor:
|
|
1079
|
+
"Implementation task needing library docs lookup (context7) or browser verification.",
|
|
1080
|
+
analyze:
|
|
1081
|
+
"Deep analysis or research task needing web search, docs lookup, and structured reasoning.",
|
|
1082
|
+
explore:
|
|
1083
|
+
"Read-only codebase and documentation exploration needing fast docs/web lookup.",
|
|
1084
|
+
reviewer:
|
|
1085
|
+
"Code review task needing minimal reference documentation lookup.",
|
|
1086
|
+
designer:
|
|
1087
|
+
"UI/frontend design task needing browser/Playwright and docs.",
|
|
1088
|
+
writer: "Documentation writing task needing docs and reference search.",
|
|
1089
|
+
default: "General task with standard MCP tool access.",
|
|
1090
|
+
},
|
|
1091
|
+
query:
|
|
1092
|
+
"Choose the best MCP server profile for this task, or 'none' if external MCP tools are unnecessary.",
|
|
1093
|
+
},
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
for (const server of candidateServers) {
|
|
1097
|
+
const safeKey = `mcp_${server.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
1098
|
+
questions[safeKey] = {
|
|
1099
|
+
hook: `task_routing_mcp_server_${server}`,
|
|
1100
|
+
kind: "noul",
|
|
1101
|
+
query: `Does this task specifically benefit from enabling the '${server}' MCP server?`,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const results = await queryJevBatch(
|
|
1106
|
+
{ state: contextBlock, questions },
|
|
1107
|
+
options,
|
|
1108
|
+
);
|
|
1109
|
+
if (!results || typeof results !== "object") return null;
|
|
1110
|
+
|
|
1111
|
+
const threshold = minConfidence(options);
|
|
1112
|
+
const compRes = results.complexity;
|
|
1113
|
+
const modeRes = results.execution_mode;
|
|
1114
|
+
const mcpProfRes = results.mcp_profile;
|
|
1115
|
+
|
|
1116
|
+
const complexity =
|
|
1117
|
+
compRes &&
|
|
1118
|
+
compRes.confidence >= threshold &&
|
|
1119
|
+
complexityOptions.includes(compRes.choice)
|
|
1120
|
+
? compRes.choice
|
|
1121
|
+
: null;
|
|
1122
|
+
|
|
1123
|
+
const executionMode =
|
|
1124
|
+
modeRes &&
|
|
1125
|
+
modeRes.confidence >= threshold &&
|
|
1126
|
+
executionModes.includes(modeRes.choice)
|
|
1127
|
+
? modeRes.choice
|
|
1128
|
+
: null;
|
|
1129
|
+
|
|
1130
|
+
const mcpProfile =
|
|
1131
|
+
mcpProfRes &&
|
|
1132
|
+
mcpProfRes.confidence >= threshold &&
|
|
1133
|
+
mcpProfiles.includes(mcpProfRes.choice)
|
|
1134
|
+
? mcpProfRes.choice
|
|
1135
|
+
: null;
|
|
1136
|
+
|
|
1137
|
+
let neededMcpServers = mcpProfile === "none" ? [] : null;
|
|
1138
|
+
if (mcpProfile !== "none" && candidateServers.length > 0) {
|
|
1139
|
+
const serverResults = candidateServers.map((server) => {
|
|
1140
|
+
const safeKey = `mcp_${server.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
1141
|
+
return results[safeKey];
|
|
1142
|
+
});
|
|
1143
|
+
// 전체에 대한 고신뢰 응답만 빈 선택과 판정 실패를 구분할 수 있다.
|
|
1144
|
+
if (
|
|
1145
|
+
serverResults.every(
|
|
1146
|
+
(res) =>
|
|
1147
|
+
res && res.confidence >= threshold && typeof res.value === "boolean",
|
|
1148
|
+
)
|
|
1149
|
+
) {
|
|
1150
|
+
neededMcpServers = candidateServers.filter(
|
|
1151
|
+
(_, idx) => serverResults[idx].value,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (
|
|
1157
|
+
!complexity &&
|
|
1158
|
+
!executionMode &&
|
|
1159
|
+
!mcpProfile &&
|
|
1160
|
+
neededMcpServers === null
|
|
1161
|
+
) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const routeSpec = complexity
|
|
1166
|
+
? COMPLEXITY_ROUTE_MAP[complexity] || null
|
|
1167
|
+
: null;
|
|
1168
|
+
|
|
1169
|
+
let codexProfile = routeSpec ? routeSpec.codexProfile : null;
|
|
1170
|
+
if (
|
|
1171
|
+
complexity === "standard" &&
|
|
1172
|
+
/deslop|cleanup|format|lint|rename|typo/i.test(trimmedPrompt)
|
|
1173
|
+
) {
|
|
1174
|
+
codexProfile = "sol_med";
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const execFlags = executionMode
|
|
1178
|
+
? executionFlagsForCli(cliType, executionMode)
|
|
1179
|
+
: null;
|
|
1180
|
+
const confidences = [
|
|
1181
|
+
complexity ? compRes : null,
|
|
1182
|
+
executionMode ? modeRes : null,
|
|
1183
|
+
mcpProfile ? mcpProfRes : null,
|
|
1184
|
+
]
|
|
1185
|
+
.filter((r) => r && Number.isFinite(r.confidence))
|
|
1186
|
+
.map((r) => r.confidence);
|
|
1187
|
+
const avgConfidence =
|
|
1188
|
+
confidences.length > 0
|
|
1189
|
+
? Number(
|
|
1190
|
+
(
|
|
1191
|
+
confidences.reduce((sum, val) => sum + val, 0) / confidences.length
|
|
1192
|
+
).toFixed(4),
|
|
1193
|
+
)
|
|
1194
|
+
: threshold;
|
|
1195
|
+
|
|
1196
|
+
return {
|
|
1197
|
+
complexity,
|
|
1198
|
+
codexProfile,
|
|
1199
|
+
agyProfile: routeSpec ? routeSpec.agyProfile : null,
|
|
1200
|
+
claudeModel: routeSpec ? routeSpec.claudeModel : null,
|
|
1201
|
+
claudeEffort: routeSpec ? routeSpec.claudeEffort : null,
|
|
1202
|
+
expectedDurationSec: routeSpec ? routeSpec.expectedDurationSec : null,
|
|
1203
|
+
executionMode,
|
|
1204
|
+
executionFlags: execFlags ? execFlags.flagsString : null,
|
|
1205
|
+
executionArgs: execFlags ? execFlags.args : null,
|
|
1206
|
+
mcpProfile,
|
|
1207
|
+
neededMcpServers,
|
|
1208
|
+
confidence: avgConfidence,
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* 7. MCP 프로필 및 서버 동적 선별 (mcp-filter 연동용).
|
|
1214
|
+
*/
|
|
1215
|
+
export async function judgeMcpSelection(
|
|
1216
|
+
{ taskText = "", agentType = "default", availableServers = [] } = {},
|
|
1217
|
+
options = {},
|
|
1218
|
+
) {
|
|
1219
|
+
if (!isJevEnabled(options)) return null;
|
|
1220
|
+
const trimmedTask = clampText(taskText, 1200);
|
|
1221
|
+
if (!trimmedTask) return null;
|
|
1222
|
+
|
|
1223
|
+
const routing = await judgeTaskRouting(
|
|
1224
|
+
{
|
|
1225
|
+
prompt: trimmedTask,
|
|
1226
|
+
agentType,
|
|
1227
|
+
cliType: "codex",
|
|
1228
|
+
availableServers,
|
|
1229
|
+
},
|
|
1230
|
+
options,
|
|
1231
|
+
);
|
|
1232
|
+
if (!routing || !Array.isArray(routing.neededMcpServers)) return null;
|
|
1233
|
+
|
|
1234
|
+
return {
|
|
1235
|
+
mcpProfile: routing.mcpProfile,
|
|
1236
|
+
neededMcpServers: routing.neededMcpServers,
|
|
1237
|
+
neededServers: routing.neededMcpServers,
|
|
1238
|
+
confidence: routing.confidence,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* 8. 다중 세션 중 프롬프트에 맞는 대상 세션 자동 선택 (--thread auto 모호성 해소).
|
|
1244
|
+
*/
|
|
1245
|
+
export async function judgeTargetSession(
|
|
1246
|
+
{ prompt = "", candidates = [] } = {},
|
|
1247
|
+
options = {},
|
|
1248
|
+
) {
|
|
1249
|
+
if (!isJevEnabled(options)) return null;
|
|
1250
|
+
const trimmedPrompt = clampText(prompt, 1000);
|
|
1251
|
+
if (!trimmedPrompt) return null;
|
|
1252
|
+
if (!Array.isArray(candidates) || candidates.length < 2) return null;
|
|
1253
|
+
|
|
1254
|
+
const normalized = candidates
|
|
1255
|
+
.map((c, idx) => {
|
|
1256
|
+
if (typeof c === "string") {
|
|
1257
|
+
return { id: c, summary: `Session ${idx + 1}: ${c}` };
|
|
1258
|
+
}
|
|
1259
|
+
const id = String(c?.id || c?.threadId || c?.name || "").trim();
|
|
1260
|
+
if (!id) return null;
|
|
1261
|
+
const summary = [
|
|
1262
|
+
c.name ? `name=${c.name}` : "",
|
|
1263
|
+
c.cwd ? `cwd=${c.cwd}` : "",
|
|
1264
|
+
c.preview ? `preview_metadata=${outputMetadata(c.preview)}` : "",
|
|
1265
|
+
c.status ? `status=${c.status}` : "",
|
|
1266
|
+
]
|
|
1267
|
+
.filter(Boolean)
|
|
1268
|
+
.join(", ");
|
|
1269
|
+
return { id, summary: summary || id };
|
|
1270
|
+
})
|
|
1271
|
+
.filter(Boolean);
|
|
1272
|
+
|
|
1273
|
+
if (normalized.length < 2) return null;
|
|
1274
|
+
|
|
1275
|
+
const allowedIds = normalized.map((c) => c.id);
|
|
1276
|
+
const candidateKeys = normalized.map((_, idx) => `candidate_${idx}`);
|
|
1277
|
+
const criteria = Object.fromEntries(
|
|
1278
|
+
normalized.map((c, idx) => [`candidate_${idx}`, `${c.id}: ${c.summary}`]),
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
const res = await queryJev(
|
|
1282
|
+
{
|
|
1283
|
+
hook: "target_session",
|
|
1284
|
+
kind: "choice",
|
|
1285
|
+
options: [...allowedIds, ...candidateKeys],
|
|
1286
|
+
criteria,
|
|
1287
|
+
query:
|
|
1288
|
+
"Multiple active worker sessions exist. Select the session ID whose working directory, topic, or recent preview best matches the incoming prompt.",
|
|
1289
|
+
context: [
|
|
1290
|
+
"Candidates:",
|
|
1291
|
+
...normalized.map(
|
|
1292
|
+
(c, idx) => `- candidate_${idx} (${c.id}): ${c.summary}`,
|
|
1293
|
+
),
|
|
1294
|
+
"",
|
|
1295
|
+
`Prompt:\n${trimmedPrompt}`,
|
|
1296
|
+
].join("\n"),
|
|
1297
|
+
},
|
|
1298
|
+
options,
|
|
1299
|
+
);
|
|
1300
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
1301
|
+
let selectedIdx = allowedIds.indexOf(res.choice);
|
|
1302
|
+
if (selectedIdx === -1) {
|
|
1303
|
+
selectedIdx = candidateKeys.indexOf(res.choice);
|
|
1304
|
+
}
|
|
1305
|
+
if (selectedIdx === -1) return null;
|
|
1306
|
+
return {
|
|
1307
|
+
sessionId: normalized[selectedIdx].id,
|
|
1308
|
+
selectedIndex: selectedIdx,
|
|
1309
|
+
selected: candidates[selectedIdx],
|
|
1310
|
+
confidence: res.confidence,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* 9. 서브태스크별 최적 워커 CLI 및 역할 배정 (orchestrator 연동용).
|
|
1316
|
+
*/
|
|
1317
|
+
export async function judgeTaskAssignment(
|
|
1318
|
+
{ subtasks = [], workers = ["codex", "claude", "antigravity"] } = {},
|
|
1319
|
+
options = {},
|
|
1320
|
+
) {
|
|
1321
|
+
if (!isJevEnabled(options)) return null;
|
|
1322
|
+
if (!Array.isArray(subtasks) || subtasks.length === 0) return null;
|
|
1323
|
+
if (!Array.isArray(workers) || workers.length === 0) return null;
|
|
1324
|
+
|
|
1325
|
+
const normalizedWorkers = workers.map((w, idx) => {
|
|
1326
|
+
if (typeof w === "string") {
|
|
1327
|
+
return {
|
|
1328
|
+
index: idx,
|
|
1329
|
+
key: `worker_${idx}`,
|
|
1330
|
+
cli: w.trim() || "codex",
|
|
1331
|
+
label: w.trim() || `worker_${idx}`,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
const cli = String(w?.cli || w?.role || "codex").trim();
|
|
1335
|
+
return {
|
|
1336
|
+
index: idx,
|
|
1337
|
+
key: `worker_${idx}`,
|
|
1338
|
+
cli,
|
|
1339
|
+
label: w?.target ? `${cli} (${w.target})` : cli,
|
|
1340
|
+
};
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
const workerKeys = normalizedWorkers.map((w) => w.key);
|
|
1344
|
+
const cliNames = normalizedWorkers.map((w) => w.cli);
|
|
1345
|
+
const allowedChoices = [...new Set([...workerKeys, ...cliNames])];
|
|
1346
|
+
|
|
1347
|
+
const defaultCriteria = {
|
|
1348
|
+
codex:
|
|
1349
|
+
"Backend implementation, refactoring, unit tests, CLI scripts, and multi-file code changes.",
|
|
1350
|
+
claude:
|
|
1351
|
+
"Deep architectural design, nuanced Korean documentation, complex reasoning, and interactive review.",
|
|
1352
|
+
antigravity:
|
|
1353
|
+
"Fast codebase exploration, frontend/UI prototyping, and lightweight verification.",
|
|
1354
|
+
};
|
|
1355
|
+
const criteria = Object.fromEntries(
|
|
1356
|
+
normalizedWorkers.map((w) => [
|
|
1357
|
+
w.key,
|
|
1358
|
+
`${w.label}: ${defaultCriteria[w.cli] || `Worker CLI option: ${w.cli}`}`,
|
|
1359
|
+
]),
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
const questions = {};
|
|
1363
|
+
const stateParts = [];
|
|
1364
|
+
subtasks.forEach((task, idx) => {
|
|
1365
|
+
const text =
|
|
1366
|
+
typeof task === "string"
|
|
1367
|
+
? task
|
|
1368
|
+
: String(task?.description || task?.title || task?.prompt || "");
|
|
1369
|
+
const clamped = clampText(text, 800);
|
|
1370
|
+
stateParts.push(`Subtask #${idx}: ${clamped}`);
|
|
1371
|
+
questions[`subtask_${idx}_worker`] = {
|
|
1372
|
+
hook: "task_assignment",
|
|
1373
|
+
kind: "choice",
|
|
1374
|
+
options: allowedChoices,
|
|
1375
|
+
criteria,
|
|
1376
|
+
query: `Select the best worker for subtask #${idx}.`,
|
|
1377
|
+
};
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
const results = await queryJevBatch(
|
|
1381
|
+
{ state: stateParts.join("\n"), questions },
|
|
1382
|
+
options,
|
|
1383
|
+
);
|
|
1384
|
+
if (!results || typeof results !== "object") return null;
|
|
1385
|
+
|
|
1386
|
+
const threshold = minConfidence(options);
|
|
1387
|
+
const assignments = subtasks.map((_, subtaskIdx) => {
|
|
1388
|
+
const res =
|
|
1389
|
+
results[`subtask_${subtaskIdx}_worker`] || results[`q_${subtaskIdx}`];
|
|
1390
|
+
let matchedWorkerIdx = subtaskIdx % normalizedWorkers.length;
|
|
1391
|
+
let valid = false;
|
|
1392
|
+
if (res && res.confidence >= threshold && res.choice) {
|
|
1393
|
+
const byKey = workerKeys.indexOf(res.choice);
|
|
1394
|
+
const byCli = cliNames.indexOf(res.choice);
|
|
1395
|
+
if (byKey !== -1) {
|
|
1396
|
+
matchedWorkerIdx = byKey;
|
|
1397
|
+
valid = true;
|
|
1398
|
+
} else if (byCli !== -1) {
|
|
1399
|
+
matchedWorkerIdx = byCli;
|
|
1400
|
+
valid = true;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const targetWorker = normalizedWorkers[matchedWorkerIdx];
|
|
1404
|
+
const cliRes = results[`subtask_${subtaskIdx}_cli`];
|
|
1405
|
+
const recommendedCli =
|
|
1406
|
+
cliRes && cliRes.confidence >= threshold && cliRes.choice
|
|
1407
|
+
? cliRes.choice
|
|
1408
|
+
: targetWorker.cli;
|
|
1409
|
+
return {
|
|
1410
|
+
index: subtaskIdx,
|
|
1411
|
+
subtaskIndex: subtaskIdx,
|
|
1412
|
+
workerIndex: matchedWorkerIdx,
|
|
1413
|
+
worker: targetWorker.cli,
|
|
1414
|
+
recommendedCli,
|
|
1415
|
+
confidence: res?.confidence ?? 0,
|
|
1416
|
+
fromJev: valid,
|
|
1417
|
+
};
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
if (!assignments.some((a) => a.fromJev)) return null;
|
|
1421
|
+
return {
|
|
1422
|
+
assignments,
|
|
1423
|
+
confidence: assignments[0]?.confidence ?? threshold,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* 10. TUI 패인 프롬프트 제출 상태 판정 (pane.mjs 연동용).
|
|
1429
|
+
*/
|
|
1430
|
+
export async function judgeComposerState(
|
|
1431
|
+
{ capturedText = "", promptPrefix = "" } = {},
|
|
1432
|
+
options = {},
|
|
1433
|
+
) {
|
|
1434
|
+
if (!isJevEnabled(options)) return null;
|
|
1435
|
+
if (!String(capturedText).trim()) return null;
|
|
1436
|
+
|
|
1437
|
+
const allowed = ["submitted_running", "stuck_in_composer", "modal_blocking"];
|
|
1438
|
+
const res = await queryJev(
|
|
1439
|
+
{
|
|
1440
|
+
hook: "composer_state",
|
|
1441
|
+
kind: "choice",
|
|
1442
|
+
options: allowed,
|
|
1443
|
+
criteria: {
|
|
1444
|
+
submitted_running:
|
|
1445
|
+
"The prompt has been submitted and the agent is now thinking, running tools, or streaming output.",
|
|
1446
|
+
stuck_in_composer:
|
|
1447
|
+
"The prompt text is still sitting unsubmitted inside the input composer box waiting for Enter.",
|
|
1448
|
+
modal_blocking:
|
|
1449
|
+
"A modal dialog, trust prompt, or confirmation popup is blocking input submission.",
|
|
1450
|
+
},
|
|
1451
|
+
query:
|
|
1452
|
+
"Inspect the terminal pane capture after sending a prompt. Determine whether the prompt was submitted and is running, is still stuck unsubmitted in the input composer, or is blocked by a modal dialog.",
|
|
1453
|
+
context: [
|
|
1454
|
+
promptPrefix
|
|
1455
|
+
? `Expected prompt prefix: ${clampText(promptPrefix, 200)}`
|
|
1456
|
+
: "",
|
|
1457
|
+
`Terminal pane metadata: ${outputMetadata(capturedText)}`,
|
|
1458
|
+
`prompt_prefix_visible: ${Boolean(promptPrefix && capturedText.includes(promptPrefix))}`,
|
|
1459
|
+
]
|
|
1460
|
+
.filter(Boolean)
|
|
1461
|
+
.join("\n\n"),
|
|
1462
|
+
},
|
|
1463
|
+
options,
|
|
1464
|
+
);
|
|
1465
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
1466
|
+
if (!allowed.includes(res.choice)) return null;
|
|
1467
|
+
return { state: res.choice, confidence: res.confidence };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* 11. 워커 개입/지시 주입 후 실제 반영 여부(Closed-loop Ack) 판정.
|
|
1472
|
+
*/
|
|
1473
|
+
export async function judgeInstructionAck(
|
|
1474
|
+
{ instruction = "", recentOutput = "" } = {},
|
|
1475
|
+
options = {},
|
|
1476
|
+
) {
|
|
1477
|
+
if (!isJevEnabled(options)) return null;
|
|
1478
|
+
const inst = clampText(instruction, 600);
|
|
1479
|
+
if (!inst || !String(recentOutput).trim()) return null;
|
|
1480
|
+
|
|
1481
|
+
const contextText = `Injected instruction:\n${inst}\n\nRecent worker output metadata: ${outputMetadata(recentOutput)}`;
|
|
1482
|
+
if (typeof options.batchClient === "function") {
|
|
1483
|
+
const batchRes = await queryJevBatch(
|
|
1484
|
+
{
|
|
1485
|
+
state: contextText,
|
|
1486
|
+
questions: {
|
|
1487
|
+
ack: {
|
|
1488
|
+
hook: "instruction_ack",
|
|
1489
|
+
kind: "noul",
|
|
1490
|
+
query:
|
|
1491
|
+
"Did the worker acknowledge, adopt, or act upon the injected instruction in its recent output?",
|
|
1492
|
+
},
|
|
1493
|
+
status: {
|
|
1494
|
+
hook: "instruction_ack_status",
|
|
1495
|
+
kind: "choice",
|
|
1496
|
+
options: [
|
|
1497
|
+
"acknowledged_executing",
|
|
1498
|
+
"acknowledged",
|
|
1499
|
+
"ignored",
|
|
1500
|
+
"blocked",
|
|
1501
|
+
],
|
|
1502
|
+
query: "Classify the worker's response to the instruction.",
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
options,
|
|
1507
|
+
);
|
|
1508
|
+
if (!batchRes?.ack) return null;
|
|
1509
|
+
if (batchRes.ack.confidence < minConfidence(options)) return null;
|
|
1510
|
+
return {
|
|
1511
|
+
acknowledged: Boolean(batchRes.ack.value),
|
|
1512
|
+
ackStatus:
|
|
1513
|
+
batchRes.status?.choice ||
|
|
1514
|
+
(batchRes.ack.value ? "acknowledged" : "ignored"),
|
|
1515
|
+
confidence: batchRes.ack.confidence,
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const res = await queryJev(
|
|
1520
|
+
{
|
|
1521
|
+
hook: "instruction_ack",
|
|
1522
|
+
kind: "noul",
|
|
1523
|
+
query:
|
|
1524
|
+
"Did the worker acknowledge, adopt, or act upon the injected instruction in its recent output?",
|
|
1525
|
+
context: contextText,
|
|
1526
|
+
},
|
|
1527
|
+
options,
|
|
1528
|
+
);
|
|
1529
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
1530
|
+
return {
|
|
1531
|
+
acknowledged: res.value,
|
|
1532
|
+
ackStatus: res.value ? "acknowledged" : "ignored",
|
|
1533
|
+
confidence: res.confidence,
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* 12. 팀 인박스 메시지 긴급도 분류 (nativeProxy / bridge team-send-message 연동용).
|
|
1539
|
+
*/
|
|
1540
|
+
export async function judgeInboxPriority(
|
|
1541
|
+
{ text = "", summary = "", from = "" } = {},
|
|
1542
|
+
options = {},
|
|
1543
|
+
) {
|
|
1544
|
+
if (!isJevEnabled(options)) return null;
|
|
1545
|
+
if (!String(text || summary).trim()) return null;
|
|
1546
|
+
|
|
1547
|
+
const allowed = ["urgent_blocker", "needs_decision", "progress_fyi", "done"];
|
|
1548
|
+
const res = await queryJev(
|
|
1549
|
+
{
|
|
1550
|
+
hook: "inbox_priority",
|
|
1551
|
+
kind: "choice",
|
|
1552
|
+
options: allowed,
|
|
1553
|
+
includeScore: true,
|
|
1554
|
+
criteria: {
|
|
1555
|
+
urgent_blocker:
|
|
1556
|
+
"Worker is completely blocked by a fatal error, missing permission, or broken environment and needs immediate lead intervention.",
|
|
1557
|
+
needs_decision:
|
|
1558
|
+
"Worker encountered an architectural or product ambiguity and needs a decision from the lead.",
|
|
1559
|
+
progress_fyi:
|
|
1560
|
+
"Routine progress update or intermediate status report that requires no immediate action.",
|
|
1561
|
+
done: "Worker completed its assigned task and is reporting final deliverables.",
|
|
1562
|
+
},
|
|
1563
|
+
query:
|
|
1564
|
+
"Classify the urgency of this worker-to-lead inbox message so the lead can prioritize responses.",
|
|
1565
|
+
context: [
|
|
1566
|
+
from ? `from: ${from}` : "",
|
|
1567
|
+
summary ? `summary_metadata: ${outputMetadata(summary)}` : "",
|
|
1568
|
+
`message_metadata: ${outputMetadata(text || summary)}`,
|
|
1569
|
+
]
|
|
1570
|
+
.filter(Boolean)
|
|
1571
|
+
.join("\n"),
|
|
1572
|
+
},
|
|
1573
|
+
options,
|
|
1574
|
+
);
|
|
1575
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
1576
|
+
if (!allowed.includes(res.choice)) return null;
|
|
1577
|
+
const priorityWeight = {
|
|
1578
|
+
urgent_blocker: 100,
|
|
1579
|
+
needs_decision: 75,
|
|
1580
|
+
done: 40,
|
|
1581
|
+
progress_fyi: 10,
|
|
1582
|
+
};
|
|
1583
|
+
return {
|
|
1584
|
+
priority: res.choice,
|
|
1585
|
+
urgencyScore:
|
|
1586
|
+
res.score ??
|
|
1587
|
+
priorityWeight[res.choice] ??
|
|
1588
|
+
Math.round(res.confidence * 100),
|
|
1589
|
+
confidence: res.confidence,
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* 13. 교차 리뷰 게이트 판정 (skip_review | cross_review | escalate_to_lead + 리뷰 루프 수렴 여부).
|
|
1595
|
+
*/
|
|
1596
|
+
export async function judgeReviewGate(
|
|
1597
|
+
{
|
|
1598
|
+
diffStat = "",
|
|
1599
|
+
handoffSummary = "",
|
|
1600
|
+
previousReviews = "",
|
|
1601
|
+
currentReview = "",
|
|
1602
|
+
} = {},
|
|
1603
|
+
options = {},
|
|
1604
|
+
) {
|
|
1605
|
+
if (!isJevEnabled(options)) return null;
|
|
1606
|
+
const summary = handoffSummary || diffStat;
|
|
1607
|
+
if (!summary && !currentReview) return null;
|
|
1608
|
+
|
|
1609
|
+
const allowedGates = ["skip_review", "cross_review", "escalate_to_lead"];
|
|
1610
|
+
const stateText = [
|
|
1611
|
+
diffStat ? `diff_stat_metadata: ${outputMetadata(diffStat)}` : "",
|
|
1612
|
+
summary ? `handoff_summary_metadata: ${outputMetadata(summary)}` : "",
|
|
1613
|
+
previousReviews
|
|
1614
|
+
? `previous_reviews_metadata: ${outputMetadata(previousReviews)}`
|
|
1615
|
+
: "",
|
|
1616
|
+
currentReview
|
|
1617
|
+
? `current_review_metadata: ${outputMetadata(currentReview)}`
|
|
1618
|
+
: "",
|
|
1619
|
+
]
|
|
1620
|
+
.filter(Boolean)
|
|
1621
|
+
.join("\n\n");
|
|
1622
|
+
|
|
1623
|
+
const questions = {
|
|
1624
|
+
gate: {
|
|
1625
|
+
hook: "review_gate",
|
|
1626
|
+
kind: "choice",
|
|
1627
|
+
options: allowedGates,
|
|
1628
|
+
criteria: {
|
|
1629
|
+
skip_review:
|
|
1630
|
+
"Trivial formatting, comment, or small isolated test change with passing tests that needs no separate cross-review.",
|
|
1631
|
+
cross_review:
|
|
1632
|
+
"Standard logic, feature, or refactoring change that benefits from automated peer cross-review.",
|
|
1633
|
+
escalate_to_lead:
|
|
1634
|
+
"High-risk architectural, security, or breaking API change that requires direct lead review.",
|
|
1635
|
+
},
|
|
1636
|
+
query:
|
|
1637
|
+
"Choose the appropriate review gate for this worker handoff and diff.",
|
|
1638
|
+
},
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
const hasReviewPair = Boolean(
|
|
1642
|
+
String(previousReviews || "").trim() && String(currentReview || "").trim(),
|
|
1643
|
+
);
|
|
1644
|
+
if (hasReviewPair) {
|
|
1645
|
+
questions.converged = {
|
|
1646
|
+
hook: "review_loop_converged",
|
|
1647
|
+
kind: "noul",
|
|
1648
|
+
query:
|
|
1649
|
+
"Has the review loop converged (either approved with no remaining blockers or repeating minor stylistic nitpicks)?",
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const results = await queryJevBatch({ state: stateText, questions }, options);
|
|
1654
|
+
if (!results?.gate) return null;
|
|
1655
|
+
|
|
1656
|
+
const threshold = minConfidence(options);
|
|
1657
|
+
const gateRes = results.gate;
|
|
1658
|
+
if (
|
|
1659
|
+
gateRes.confidence < threshold ||
|
|
1660
|
+
!allowedGates.includes(gateRes.choice)
|
|
1661
|
+
) {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const loopRes = hasReviewPair ? results.converged : null;
|
|
1666
|
+
const converged =
|
|
1667
|
+
loopRes && loopRes.confidence >= threshold ? Boolean(loopRes.value) : false;
|
|
1668
|
+
|
|
1669
|
+
return {
|
|
1670
|
+
gate: gateRes.choice,
|
|
1671
|
+
reviewGate: gateRes.choice,
|
|
1672
|
+
converged,
|
|
1673
|
+
confidence: gateRes.confidence,
|
|
1674
|
+
};
|
|
1675
|
+
}
|