@triflux/core 10.50.1 → 10.50.2
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/index.mjs +2 -8
- package/hub/lib/cto-env.mjs +0 -23
- package/hub/paths.mjs +0 -6
- package/hub/pipeline/index.mjs +0 -190
- package/hub/pipeline/transitions.mjs +0 -10
- package/hub/reflexion.mjs +1 -84
- package/hud/cli-policy.mjs +1 -5
- package/hud/colors.mjs +0 -13
- package/hud/constants.mjs +4 -135
- package/hud/context-monitor.mjs +3 -64
- package/hud/hud-qos-status.mjs +43 -225
- package/hud/providers/claude.mjs +4 -16
- package/hud/providers/gemini.mjs +7 -698
- package/hud/renderers.mjs +40 -294
- package/hud/terminal.mjs +1 -63
- package/hud/utils.mjs +0 -24
- package/package.json +1 -2
- package/scripts/lib/gemini-profiles.mjs +1 -1
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/psmux-info.mjs +18 -49
- package/scripts/lib/skill-template.mjs +0 -26
- package/hub/codex-compat.mjs +0 -12
- package/hub/fullcycle.mjs +0 -113
- package/hub/gemini-adapter.mjs +0 -174
- package/hub/intent.mjs +0 -321
- package/hub/lib/mcp-response-cache.mjs +0 -239
- package/hub/lib/path-utils.mjs +0 -165
- package/hub/lib/timeline-adapter.mjs +0 -190
- package/hub/pipeline/gates/confidence.mjs +0 -75
- package/hub/pipeline/gates/consensus.mjs +0 -106
- package/hub/pipeline/gates/index.mjs +0 -9
- package/hub/pipeline/gates/selfcheck.mjs +0 -120
- package/hub/quality/deslop.mjs +0 -274
- package/hub/research.mjs +0 -155
- package/hub/role-control-events.mjs +0 -113
- package/hub/routing/complexity.mjs +0 -269
- package/hub/routing/index.mjs +0 -122
- package/hub/routing/q-learning.mjs +0 -387
- package/hub/token-mode.mjs +0 -228
- package/hud/mission-board.mjs +0 -56
- package/hud/providers/cto.mjs +0 -76
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
// hub/routing/q-learning.mjs — 테이블 기반 Q-Learning 동적 라우팅
|
|
2
|
-
// agent-map.json 폴백을 유지하면서, 작업 결과 피드백으로 가중치를 학습한다.
|
|
3
|
-
// 외부 의존성 없음 (fs, path, crypto만 사용)
|
|
4
|
-
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { createRequire } from "node:module";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { scoreComplexity } from "./complexity.mjs";
|
|
11
|
-
|
|
12
|
-
const _require = createRequire(import.meta.url);
|
|
13
|
-
|
|
14
|
-
/** agent-map.json 정적 매핑 (폴백용) */
|
|
15
|
-
let AGENT_MAP;
|
|
16
|
-
try {
|
|
17
|
-
AGENT_MAP = _require("../team/agent-map.json");
|
|
18
|
-
} catch {
|
|
19
|
-
AGENT_MAP = {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** 사용 가능한 CLI 액션 */
|
|
23
|
-
const ACTIONS = ["codex", "antigravity", "claude", "haiku", "sonnet"];
|
|
24
|
-
|
|
25
|
-
/** 특성 벡터 키워드 (48차원, 에이전트 타입 기반) */
|
|
26
|
-
const FEATURE_KEYWORDS = [
|
|
27
|
-
// 실행/구현 (codex 친화)
|
|
28
|
-
"implement",
|
|
29
|
-
"execute",
|
|
30
|
-
"build",
|
|
31
|
-
"fix",
|
|
32
|
-
"debug",
|
|
33
|
-
"code",
|
|
34
|
-
"refactor",
|
|
35
|
-
"test",
|
|
36
|
-
// 분석/설계 (claude/sonnet 친화)
|
|
37
|
-
"analyze",
|
|
38
|
-
"architect",
|
|
39
|
-
"plan",
|
|
40
|
-
"review",
|
|
41
|
-
"security",
|
|
42
|
-
"optimize",
|
|
43
|
-
"research",
|
|
44
|
-
"evaluate",
|
|
45
|
-
// 디자인/문서 (Antigravity 친화)
|
|
46
|
-
"design",
|
|
47
|
-
"ui",
|
|
48
|
-
"ux",
|
|
49
|
-
"frontend",
|
|
50
|
-
"visual",
|
|
51
|
-
"document",
|
|
52
|
-
"write",
|
|
53
|
-
"explain",
|
|
54
|
-
// 간단/빠른 (haiku 친화)
|
|
55
|
-
"simple",
|
|
56
|
-
"quick",
|
|
57
|
-
"trivial",
|
|
58
|
-
"rename",
|
|
59
|
-
"format",
|
|
60
|
-
"lint",
|
|
61
|
-
"typo",
|
|
62
|
-
"minor",
|
|
63
|
-
// 한국어 — 실행/구현 (codex 친화)
|
|
64
|
-
"구현",
|
|
65
|
-
"빌드",
|
|
66
|
-
"수정",
|
|
67
|
-
"디버깅",
|
|
68
|
-
"리팩터링",
|
|
69
|
-
"테스트",
|
|
70
|
-
// 한국어 — 분석/설계 (claude/sonnet 친화)
|
|
71
|
-
"분석",
|
|
72
|
-
"아키텍처",
|
|
73
|
-
"설계",
|
|
74
|
-
"검토",
|
|
75
|
-
"보안",
|
|
76
|
-
"최적화",
|
|
77
|
-
// 한국어 — 디자인/문서 (Antigravity 친화)
|
|
78
|
-
"디자인",
|
|
79
|
-
"문서화",
|
|
80
|
-
// 한국어 — 간단/빠른 (haiku 친화)
|
|
81
|
-
"간단",
|
|
82
|
-
"사소한",
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 텍스트에서 48차원 특성 벡터 추출
|
|
87
|
-
* 단어 경계를 기준으로 매칭하여 부분 문자열 오탐을 방지한다.
|
|
88
|
-
* @param {string} text
|
|
89
|
-
* @returns {number[]} 48-dim binary feature vector
|
|
90
|
-
*/
|
|
91
|
-
function extractFeatures(text) {
|
|
92
|
-
const lower = text.toLowerCase();
|
|
93
|
-
return FEATURE_KEYWORDS.map((kw) => {
|
|
94
|
-
// 영문 단일 단어: 단어 경계(\b) 적용
|
|
95
|
-
// 한국어 또는 다중 단어 구문: 공백/문장 경계 기반 포함 여부 확인
|
|
96
|
-
if (/^[a-z]+$/.test(kw)) {
|
|
97
|
-
return new RegExp(`\\b${kw}\\b`).test(lower) ? 1 : 0;
|
|
98
|
-
}
|
|
99
|
-
return lower.includes(kw) ? 1 : 0;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 특성 벡터를 상태 키로 변환 (해시 기반)
|
|
105
|
-
* @param {number[]} features
|
|
106
|
-
* @returns {string}
|
|
107
|
-
*/
|
|
108
|
-
function stateKey(features) {
|
|
109
|
-
const hash = createHash("sha256").update(features.join(",")).digest("hex");
|
|
110
|
-
return hash.slice(0, 16);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* LRU 캐시 (예측 결과 캐싱)
|
|
115
|
-
*/
|
|
116
|
-
class LRUCache {
|
|
117
|
-
/** @param {number} maxSize @param {number} ttlMs */
|
|
118
|
-
constructor(maxSize = 256, ttlMs = 5 * 60 * 1000) {
|
|
119
|
-
this._maxSize = maxSize;
|
|
120
|
-
this._ttlMs = ttlMs;
|
|
121
|
-
/** @type {Map<string, { value: *, ts: number }>} */
|
|
122
|
-
this._cache = new Map();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
get(key) {
|
|
126
|
-
const entry = this._cache.get(key);
|
|
127
|
-
if (!entry) return undefined;
|
|
128
|
-
if (Date.now() - entry.ts > this._ttlMs) {
|
|
129
|
-
this._cache.delete(key);
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
// LRU: 다시 삽입하여 순서 갱신
|
|
133
|
-
this._cache.delete(key);
|
|
134
|
-
this._cache.set(key, entry);
|
|
135
|
-
return entry.value;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
set(key, value) {
|
|
139
|
-
if (this._cache.has(key)) this._cache.delete(key);
|
|
140
|
-
this._cache.set(key, { value, ts: Date.now() });
|
|
141
|
-
// 초과 시 가장 오래된 항목 제거
|
|
142
|
-
if (this._cache.size > this._maxSize) {
|
|
143
|
-
const oldest = this._cache.keys().next().value;
|
|
144
|
-
this._cache.delete(oldest);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
clear() {
|
|
149
|
-
this._cache.clear();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Q-Learning 라우터
|
|
155
|
-
* 테이블 기반 Q-Learning으로 작업→CLI 매핑을 학습한다.
|
|
156
|
-
*/
|
|
157
|
-
export class QLearningRouter {
|
|
158
|
-
/**
|
|
159
|
-
* @param {object} [opts]
|
|
160
|
-
* @param {number} [opts.learningRate=0.1] — 학습률 (alpha)
|
|
161
|
-
* @param {number} [opts.discountFactor=0.9] — 할인율 (gamma)
|
|
162
|
-
* @param {number} [opts.epsilon=0.3] — 탐색 확률 (epsilon-greedy)
|
|
163
|
-
* @param {number} [opts.epsilonDecay=0.995] — 엡실론 감쇠율
|
|
164
|
-
* @param {number} [opts.epsilonMin=0.05] — 최소 엡실론
|
|
165
|
-
* @param {number} [opts.minConfidence=0.6] — 최소 신뢰도 (이하면 폴백)
|
|
166
|
-
* @param {string} [opts.modelPath] — Q-table 영속화 경로
|
|
167
|
-
* @param {() => number} [opts.random=Math.random] — 테스트/재현용 RNG seam
|
|
168
|
-
*/
|
|
169
|
-
constructor(opts = {}) {
|
|
170
|
-
this._lr = opts.learningRate ?? 0.1;
|
|
171
|
-
this._gamma = opts.discountFactor ?? 0.9;
|
|
172
|
-
this._epsilon = opts.epsilon ?? 0.3;
|
|
173
|
-
this._epsilonDecay = opts.epsilonDecay ?? 0.995;
|
|
174
|
-
// epsilon=0 시에도 최소 탐색 보장 (pure-exploit 방지)
|
|
175
|
-
this._epsilonMin =
|
|
176
|
-
opts.epsilonMin ?? Math.max(0.01, Math.min(0.05, this._epsilon));
|
|
177
|
-
this._minConfidence = opts.minConfidence ?? 0.6;
|
|
178
|
-
this._modelPath =
|
|
179
|
-
opts.modelPath ?? join(homedir(), ".omc", "routing-model.json");
|
|
180
|
-
this._random =
|
|
181
|
-
typeof opts.random === "function" ? opts.random : Math.random;
|
|
182
|
-
|
|
183
|
-
/** @type {Map<string, Map<string, number>>} state -> (action -> Q-value) */
|
|
184
|
-
this._qTable = new Map();
|
|
185
|
-
|
|
186
|
-
/** @type {Map<string, number>} state -> visit count */
|
|
187
|
-
this._visitCounts = new Map();
|
|
188
|
-
|
|
189
|
-
/** 총 업데이트 횟수 */
|
|
190
|
-
this._totalUpdates = 0;
|
|
191
|
-
|
|
192
|
-
/** 예측 캐시 */
|
|
193
|
-
this._cache = new LRUCache(256, 5 * 60 * 1000);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* 상태에 대한 Q-values 조회 (없으면 초기화)
|
|
198
|
-
* @param {string} state
|
|
199
|
-
* @returns {Map<string, number>}
|
|
200
|
-
*/
|
|
201
|
-
_getQValues(state) {
|
|
202
|
-
if (!this._qTable.has(state)) {
|
|
203
|
-
const qValues = new Map();
|
|
204
|
-
for (const action of ACTIONS) {
|
|
205
|
-
qValues.set(action, 0);
|
|
206
|
-
}
|
|
207
|
-
this._qTable.set(state, qValues);
|
|
208
|
-
}
|
|
209
|
-
return this._qTable.get(state);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* 작업 설명으로부터 최적 CLI 타입 예측
|
|
214
|
-
* @param {string} taskDescription
|
|
215
|
-
* @returns {{ action: string, confidence: number, exploration: boolean, complexity: number }}
|
|
216
|
-
*/
|
|
217
|
-
predict(taskDescription) {
|
|
218
|
-
const features = extractFeatures(taskDescription);
|
|
219
|
-
const state = stateKey(features);
|
|
220
|
-
|
|
221
|
-
// 캐시 확인
|
|
222
|
-
const cached = this._cache.get(state);
|
|
223
|
-
if (cached) return cached;
|
|
224
|
-
|
|
225
|
-
const qValues = this._getQValues(state);
|
|
226
|
-
const visits = this._visitCounts.get(state) || 0;
|
|
227
|
-
|
|
228
|
-
// 엡실론-그리디: 탐색 vs 활용
|
|
229
|
-
const isExploration = this._random() < this._epsilon;
|
|
230
|
-
|
|
231
|
-
let action;
|
|
232
|
-
if (isExploration) {
|
|
233
|
-
// 무작위 탐색
|
|
234
|
-
action = ACTIONS[Math.floor(this._random() * ACTIONS.length)];
|
|
235
|
-
} else {
|
|
236
|
-
// 최적 액션 선택 (최대 Q-value)
|
|
237
|
-
let maxQ = -Infinity;
|
|
238
|
-
action = ACTIONS[0];
|
|
239
|
-
for (const [a, q] of qValues) {
|
|
240
|
-
if (q > maxQ) {
|
|
241
|
-
maxQ = q;
|
|
242
|
-
action = a;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// 신뢰도 계산: 방문 횟수 기반 (최소 10회 이상이면 안정)
|
|
248
|
-
const confidence = visits >= 10 ? Math.min(visits / 50, 1) : visits / 10;
|
|
249
|
-
|
|
250
|
-
const { score: complexity } = scoreComplexity(taskDescription);
|
|
251
|
-
|
|
252
|
-
const result = {
|
|
253
|
-
action,
|
|
254
|
-
confidence,
|
|
255
|
-
exploration: isExploration,
|
|
256
|
-
complexity,
|
|
257
|
-
};
|
|
258
|
-
// 탐색(랜덤) 결과는 캐싱하지 않음 — 매번 새로운 랜덤 액션 생성
|
|
259
|
-
if (!isExploration) this._cache.set(state, result);
|
|
260
|
-
return result;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Q-table 업데이트 (보상 피드백)
|
|
265
|
-
* @param {string} taskDescription — 작업 설명
|
|
266
|
-
* @param {string} action — 수행한 액션 (CLI 타입)
|
|
267
|
-
* @param {number} reward — 보상 (-1 ~ 1)
|
|
268
|
-
*/
|
|
269
|
-
update(taskDescription, action, reward) {
|
|
270
|
-
if (!ACTIONS.includes(action)) return;
|
|
271
|
-
|
|
272
|
-
const features = extractFeatures(taskDescription);
|
|
273
|
-
const state = stateKey(features);
|
|
274
|
-
const qValues = this._getQValues(state);
|
|
275
|
-
const oldQ = qValues.get(action) || 0;
|
|
276
|
-
|
|
277
|
-
// 최대 미래 Q-value (단일 상태이므로 현재 상태의 max)
|
|
278
|
-
let maxFutureQ = -Infinity;
|
|
279
|
-
for (const [, q] of qValues) {
|
|
280
|
-
if (q > maxFutureQ) maxFutureQ = q;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Q-Learning 업데이트: Q(s,a) = Q(s,a) + lr * (reward + gamma * max_Q(s') - Q(s,a))
|
|
284
|
-
const newQ = oldQ + this._lr * (reward + this._gamma * maxFutureQ - oldQ);
|
|
285
|
-
qValues.set(action, newQ);
|
|
286
|
-
|
|
287
|
-
// 방문 횟수 증가
|
|
288
|
-
this._visitCounts.set(state, (this._visitCounts.get(state) || 0) + 1);
|
|
289
|
-
this._totalUpdates++;
|
|
290
|
-
|
|
291
|
-
// 엡실론 감쇠
|
|
292
|
-
this._epsilon = Math.max(
|
|
293
|
-
this._epsilonMin,
|
|
294
|
-
this._epsilon * this._epsilonDecay,
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
// 캐시 무효화 (해당 상태)
|
|
298
|
-
this._cache.clear();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Q-table을 JSON 파일로 영속화
|
|
303
|
-
*/
|
|
304
|
-
save() {
|
|
305
|
-
const dir = join(this._modelPath, "..");
|
|
306
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
307
|
-
|
|
308
|
-
const data = {
|
|
309
|
-
version: 1,
|
|
310
|
-
epsilon: this._epsilon,
|
|
311
|
-
totalUpdates: this._totalUpdates,
|
|
312
|
-
qTable: {},
|
|
313
|
-
visitCounts: {},
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
for (const [state, qValues] of this._qTable) {
|
|
317
|
-
data.qTable[state] = Object.fromEntries(qValues);
|
|
318
|
-
}
|
|
319
|
-
for (const [state, count] of this._visitCounts) {
|
|
320
|
-
data.visitCounts[state] = count;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
writeFileSync(this._modelPath, JSON.stringify(data, null, 2), "utf8");
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* 영속화된 Q-table 로드
|
|
328
|
-
* @returns {boolean} 로드 성공 여부
|
|
329
|
-
*/
|
|
330
|
-
load() {
|
|
331
|
-
if (!existsSync(this._modelPath)) return false;
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
const raw = readFileSync(this._modelPath, "utf8");
|
|
335
|
-
const data = JSON.parse(raw);
|
|
336
|
-
if (data.version !== 1) return false;
|
|
337
|
-
|
|
338
|
-
this._epsilon = data.epsilon ?? this._epsilon;
|
|
339
|
-
this._totalUpdates = data.totalUpdates ?? 0;
|
|
340
|
-
this._qTable = new Map();
|
|
341
|
-
this._visitCounts = new Map();
|
|
342
|
-
|
|
343
|
-
for (const [state, qObj] of Object.entries(data.qTable || {})) {
|
|
344
|
-
const qValues = new Map();
|
|
345
|
-
for (const [action, q] of Object.entries(qObj)) {
|
|
346
|
-
if (ACTIONS.includes(action)) qValues.set(action, q);
|
|
347
|
-
}
|
|
348
|
-
this._qTable.set(state, qValues);
|
|
349
|
-
}
|
|
350
|
-
for (const [state, count] of Object.entries(data.visitCounts || {})) {
|
|
351
|
-
this._visitCounts.set(state, count);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
this._cache.clear();
|
|
355
|
-
return true;
|
|
356
|
-
} catch {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* agent-map.json 폴백 조회
|
|
363
|
-
* @param {string} agentType — 에이전트 역할명
|
|
364
|
-
* @returns {string} CLI 타입
|
|
365
|
-
*/
|
|
366
|
-
static fallback(agentType) {
|
|
367
|
-
return AGENT_MAP[agentType] || agentType;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** 현재 엡실론 값 */
|
|
371
|
-
get epsilon() {
|
|
372
|
-
return this._epsilon;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** 총 업데이트 횟수 */
|
|
376
|
-
get totalUpdates() {
|
|
377
|
-
return this._totalUpdates;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Q-table 상태 수 */
|
|
381
|
-
get stateCount() {
|
|
382
|
-
return this._qTable.size;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// 모듈 레벨 export
|
|
387
|
-
export { ACTIONS, extractFeatures, FEATURE_KEYWORDS, LRUCache, stateKey };
|
package/hub/token-mode.mjs
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
// hub/token-mode.mjs — Token Efficiency Mode
|
|
2
|
-
// GAP 분석 P2 #7: 심볼 통신 + 약어로 30-50% 토큰 절감
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @experimental 런타임 미연결 — 향후 통합 예정
|
|
6
|
-
* @experimental
|
|
7
|
-
* @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>}
|
|
8
|
-
*/
|
|
9
|
-
export const COMPACT_RULES = [
|
|
10
|
-
// ── 심볼 치환 (긴 매칭 우선 정렬) ──
|
|
11
|
-
{ from: ["greater than or equal"], to: "≥", type: "symbol" },
|
|
12
|
-
{ from: ["less than or equal"], to: "≤", type: "symbol" },
|
|
13
|
-
{ from: ["results in", "결과적으로"], to: "→", type: "symbol" },
|
|
14
|
-
{ from: ["therefore", "따라서"], to: "∴", type: "symbol" },
|
|
15
|
-
{ from: ["because", "왜냐하면"], to: "∵", type: "symbol" },
|
|
16
|
-
{ from: ["approximately", "대략"], to: "≈", type: "symbol" },
|
|
17
|
-
{ from: ["not equal", "같지 않"], to: "≠", type: "symbol" },
|
|
18
|
-
{ from: ["in progress", "진행 중"], to: "⏳", type: "symbol" },
|
|
19
|
-
{ from: ["completed", "완료"], to: "✓", type: "symbol" },
|
|
20
|
-
{ from: ["success", "성공"], to: "✓", type: "symbol" },
|
|
21
|
-
{ from: ["failure", "실패"], to: "✗", type: "symbol" },
|
|
22
|
-
{ from: ["warning", "경고"], to: "⚠", type: "symbol" },
|
|
23
|
-
{ from: ["error", "에러"], to: "✗", type: "symbol" },
|
|
24
|
-
{ from: ["pending", "대기"], to: "⏸", type: "symbol" },
|
|
25
|
-
|
|
26
|
-
// ── 약어 (긴 매칭 우선 정렬) ──
|
|
27
|
-
{ from: ["configuration", "설정"], to: "cfg", type: "abbrev" },
|
|
28
|
-
{ from: ["implementation", "구현"], to: "impl", type: "abbrev" },
|
|
29
|
-
{ from: ["architecture", "아키텍처"], to: "arch", type: "abbrev" },
|
|
30
|
-
{ from: ["dependency", "의존성"], to: "dep", type: "abbrev" },
|
|
31
|
-
{ from: ["function", "함수"], to: "fn", type: "abbrev" },
|
|
32
|
-
{ from: ["parameter", "파라미터"], to: "param", type: "abbrev" },
|
|
33
|
-
{ from: ["repository", "저장소"], to: "repo", type: "abbrev" },
|
|
34
|
-
{ from: ["environment", "환경"], to: "env", type: "abbrev" },
|
|
35
|
-
{ from: ["variable", "변수"], to: "var", type: "abbrev" },
|
|
36
|
-
{ from: ["directory", "디렉토리"], to: "dir", type: "abbrev" },
|
|
37
|
-
|
|
38
|
-
// ── 한국어 동사/명령형 약어 ──
|
|
39
|
-
{ from: ["구현해"], to: "impl", type: "abbrev" },
|
|
40
|
-
{ from: ["확인해"], to: "check", type: "abbrev" },
|
|
41
|
-
{ from: ["수정해"], to: "fix", type: "abbrev" },
|
|
42
|
-
{ from: ["테스트"], to: "test", type: "abbrev" },
|
|
43
|
-
{ from: ["리뷰"], to: "review", type: "abbrev" },
|
|
44
|
-
{ from: ["분석"], to: "analyze", type: "abbrev" },
|
|
45
|
-
{ from: ["설계"], to: "design", type: "abbrev" },
|
|
46
|
-
{ from: ["문서화"], to: "docs", type: "abbrev" },
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
|
|
50
|
-
export const REVIEW_RULES = [
|
|
51
|
-
{ from: ["looks good to me", "lgtm"], to: "✓lgtm", type: "abbrev" },
|
|
52
|
-
{ from: ["needs changes", "수정 필요"], to: "✗chg", type: "abbrev" },
|
|
53
|
-
{ from: ["nitpick", "사소한"], to: "nit", type: "abbrev" },
|
|
54
|
-
{ from: ["blocking", "블로킹"], to: "blk", type: "abbrev" },
|
|
55
|
-
{ from: ["suggestion", "제안"], to: "sug", type: "abbrev" },
|
|
56
|
-
{ from: ["question", "질문"], to: "q", type: "abbrev" },
|
|
57
|
-
{ from: ["approved", "승인"], to: "✓apv", type: "abbrev" },
|
|
58
|
-
{ from: ["request changes", "변경 요청"], to: "✗req", type: "abbrev" },
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
/** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
|
|
62
|
-
export const DESIGN_RULES = [
|
|
63
|
-
{ from: ["component", "컴포넌트"], to: "cmp", type: "abbrev" },
|
|
64
|
-
{ from: ["interface", "인터페이스"], to: "iface", type: "abbrev" },
|
|
65
|
-
{ from: ["abstraction", "추상화"], to: "abs", type: "abbrev" },
|
|
66
|
-
{ from: ["pattern", "패턴"], to: "ptn", type: "abbrev" },
|
|
67
|
-
{ from: ["dependency injection", "의존성 주입"], to: "di", type: "abbrev" },
|
|
68
|
-
{ from: ["single responsibility", "단일 책임"], to: "srp", type: "abbrev" },
|
|
69
|
-
{ from: ["open closed", "개방 폐쇄"], to: "ocp", type: "abbrev" },
|
|
70
|
-
{ from: ["inheritance", "상속"], to: "inh", type: "abbrev" },
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
/** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
|
|
74
|
-
export const DOCS_RULES = [
|
|
75
|
-
{ from: ["description", "설명"], to: "desc", type: "abbrev" },
|
|
76
|
-
{ from: ["example", "예시"], to: "ex", type: "abbrev" },
|
|
77
|
-
{ from: ["reference", "참조"], to: "ref", type: "abbrev" },
|
|
78
|
-
{ from: ["introduction", "소개"], to: "intro", type: "abbrev" },
|
|
79
|
-
{ from: ["deprecated", "사용 중단"], to: "dep", type: "abbrev" },
|
|
80
|
-
{ from: ["optional", "선택적"], to: "opt", type: "abbrev" },
|
|
81
|
-
{ from: ["required", "필수"], to: "req", type: "abbrev" },
|
|
82
|
-
{ from: ["returns", "반환"], to: "ret", type: "abbrev" },
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
// ── 프로필 맵 ──
|
|
86
|
-
|
|
87
|
-
/** @type {Record<string, Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>>} */
|
|
88
|
-
const PROFILE_MAP = {
|
|
89
|
-
default: COMPACT_RULES,
|
|
90
|
-
review: [...COMPACT_RULES, ...REVIEW_RULES],
|
|
91
|
-
design: [...COMPACT_RULES, ...DESIGN_RULES],
|
|
92
|
-
docs: [...COMPACT_RULES, ...DOCS_RULES],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// ── 내부: 정렬된 치환 쌍 빌드 헬퍼 ──
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 규칙 배열로부터 패턴 쌍을 빌드하고 긴 패턴 우선 정렬
|
|
99
|
-
* @param {Array<{ from: string[], to: string }>} rules
|
|
100
|
-
* @returns {Array<{ pattern: string, to: string, len: number }>}
|
|
101
|
-
*/
|
|
102
|
-
function buildCompactPairs(rules) {
|
|
103
|
-
const pairs = [];
|
|
104
|
-
for (const rule of rules) {
|
|
105
|
-
for (const keyword of rule.from) {
|
|
106
|
-
pairs.push({ pattern: keyword, to: rule.to, len: keyword.length });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
pairs.sort((a, b) => b.len - a.len);
|
|
110
|
-
return pairs;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 규칙 배열로부터 확장 쌍을 빌드
|
|
115
|
-
* @param {Array<{ from: string[], to: string }>} rules
|
|
116
|
-
* @returns {Array<{ symbol: string, restore: string }>}
|
|
117
|
-
*/
|
|
118
|
-
function buildExpandPairs(rules) {
|
|
119
|
-
return rules.map((rule) => ({ symbol: rule.to, restore: rule.from[0] }));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 기본 프로필 쌍 (기존 동작 유지)
|
|
123
|
-
const _compactPairs = buildCompactPairs(COMPACT_RULES);
|
|
124
|
-
const _expandPairs = buildExpandPairs(COMPACT_RULES);
|
|
125
|
-
|
|
126
|
-
// ── 코드 블록 보호 유틸 ──
|
|
127
|
-
|
|
128
|
-
const CODE_BLOCK_RE = /```[\s\S]*?```/g;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 코드 블록을 플레이스홀더로 치환, 변환 후 복원
|
|
132
|
-
* @param {string} text
|
|
133
|
-
* @param {(segment: string) => string} transform
|
|
134
|
-
* @returns {string}
|
|
135
|
-
*/
|
|
136
|
-
function withCodeProtection(text, transform) {
|
|
137
|
-
const blocks = [];
|
|
138
|
-
const placeholder = "\x00CB";
|
|
139
|
-
let idx = 0;
|
|
140
|
-
const masked = text.replace(CODE_BLOCK_RE, (match) => {
|
|
141
|
-
blocks.push(match);
|
|
142
|
-
return `${placeholder}${idx++}${placeholder}`;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const transformed = transform(masked);
|
|
146
|
-
|
|
147
|
-
// 플레이스홀더 복원
|
|
148
|
-
return transformed.replace(
|
|
149
|
-
new RegExp(`${placeholder}(\\d+)${placeholder}`, "g"),
|
|
150
|
-
(_, i) => blocks[Number(i)],
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ── compact 모드 상태 ──
|
|
155
|
-
|
|
156
|
-
let _compactMode = false;
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* compact 모드 활성 여부
|
|
160
|
-
* @returns {boolean}
|
|
161
|
-
*/
|
|
162
|
-
export function isCompactMode() {
|
|
163
|
-
return _compactMode;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 텍스트를 compact 모드로 변환
|
|
168
|
-
* 심볼 치환 + 약어 적용, 코드 블록 내부는 보호
|
|
169
|
-
* @param {string} text
|
|
170
|
-
* @returns {string}
|
|
171
|
-
*/
|
|
172
|
-
export function compactify(text) {
|
|
173
|
-
if (!text || typeof text !== "string") return text ?? "";
|
|
174
|
-
_compactMode = true;
|
|
175
|
-
|
|
176
|
-
return withCodeProtection(text, (segment) => {
|
|
177
|
-
let result = segment;
|
|
178
|
-
for (const { pattern, to } of _compactPairs) {
|
|
179
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
180
|
-
const re = new RegExp(escaped, "gi");
|
|
181
|
-
result = result.replace(re, to);
|
|
182
|
-
}
|
|
183
|
-
return result;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* 텍스트를 compact 모드로 변환 (도메인 프로필 선택 가능)
|
|
189
|
-
* @param {string} text
|
|
190
|
-
* @param {'default'|'review'|'design'|'docs'} [profile='default'] — 도메인 프로필
|
|
191
|
-
* @returns {string}
|
|
192
|
-
*/
|
|
193
|
-
export function applyCompactRules(text, profile = "default") {
|
|
194
|
-
if (!text || typeof text !== "string") return text ?? "";
|
|
195
|
-
|
|
196
|
-
const rules = PROFILE_MAP[profile] ?? COMPACT_RULES;
|
|
197
|
-
const pairs = buildCompactPairs(rules);
|
|
198
|
-
|
|
199
|
-
return withCodeProtection(text, (segment) => {
|
|
200
|
-
let result = segment;
|
|
201
|
-
for (const { pattern, to } of pairs) {
|
|
202
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
203
|
-
const re = new RegExp(escaped, "gi");
|
|
204
|
-
result = result.replace(re, to);
|
|
205
|
-
}
|
|
206
|
-
return result;
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* compact 텍스트를 원래 형태로 복원 (best-effort)
|
|
212
|
-
* @param {string} text
|
|
213
|
-
* @returns {string}
|
|
214
|
-
*/
|
|
215
|
-
export function expand(text) {
|
|
216
|
-
if (!text || typeof text !== "string") return text ?? "";
|
|
217
|
-
_compactMode = false;
|
|
218
|
-
|
|
219
|
-
return withCodeProtection(text, (segment) => {
|
|
220
|
-
let result = segment;
|
|
221
|
-
for (const { symbol, restore } of _expandPairs) {
|
|
222
|
-
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
223
|
-
const re = new RegExp(escaped, "g");
|
|
224
|
-
result = result.replace(re, restore);
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
});
|
|
228
|
-
}
|
package/hud/mission-board.mjs
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// HUD Mission Board — .omc/state/sessions/ 기반 에이전트 진행률 집계
|
|
3
|
-
// ============================================================================
|
|
4
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
const SESSIONS_DIR = join(homedir(), ".omc", "state", "sessions");
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* .omc/state/sessions/ 디렉토리를 읽어 팀 상태를 반환한다.
|
|
12
|
-
* @returns {{ agents: Array<{name: string, status: string, progress: number}>, dagLevel: number, totalProgress: number } | null}
|
|
13
|
-
*/
|
|
14
|
-
export async function getMissionBoardState(sessionsDir = SESSIONS_DIR) {
|
|
15
|
-
let entries;
|
|
16
|
-
try {
|
|
17
|
-
entries = await readdir(sessionsDir);
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const jsonFiles = entries.filter((f) => f.endsWith(".json"));
|
|
23
|
-
if (jsonFiles.length === 0) return null;
|
|
24
|
-
|
|
25
|
-
const agents = [];
|
|
26
|
-
for (const file of jsonFiles) {
|
|
27
|
-
let data;
|
|
28
|
-
try {
|
|
29
|
-
const raw = await readFile(join(sessionsDir, file), "utf8");
|
|
30
|
-
data = JSON.parse(raw);
|
|
31
|
-
} catch {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const name = data.name ?? file.replace(/\.json$/, "");
|
|
36
|
-
const status = data.status ?? "idle";
|
|
37
|
-
const progress = typeof data.progress === "number" ? data.progress : 0;
|
|
38
|
-
agents.push({ name, status, progress });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (agents.length === 0) return null;
|
|
42
|
-
|
|
43
|
-
const totalProgress =
|
|
44
|
-
agents.length > 0
|
|
45
|
-
? Math.round(
|
|
46
|
-
agents.reduce((sum, a) => sum + a.progress, 0) / agents.length,
|
|
47
|
-
)
|
|
48
|
-
: 0;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
agents,
|
|
52
|
-
// TODO: derive dagLevel from real mission dependency metadata instead of hardcoding 0.
|
|
53
|
-
dagLevel: 0,
|
|
54
|
-
totalProgress,
|
|
55
|
-
};
|
|
56
|
-
}
|