@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.
Files changed (55) hide show
  1. package/hub/index.mjs +2 -8
  2. package/hub/lib/cto-env.mjs +0 -23
  3. package/hub/paths.mjs +0 -6
  4. package/hub/pipeline/index.mjs +0 -190
  5. package/hub/pipeline/transitions.mjs +0 -10
  6. package/hub/reflexion.mjs +1 -84
  7. package/hud/cli-policy.mjs +1 -5
  8. package/hud/colors.mjs +0 -13
  9. package/hud/constants.mjs +4 -135
  10. package/hud/context-monitor.mjs +3 -64
  11. package/hud/hud-qos-status.mjs +43 -225
  12. package/hud/providers/claude.mjs +4 -16
  13. package/hud/providers/gemini.mjs +7 -698
  14. package/hud/renderers.mjs +40 -294
  15. package/hud/terminal.mjs +1 -63
  16. package/hud/utils.mjs +0 -24
  17. package/package.json +1 -2
  18. package/scripts/lib/gemini-profiles.mjs +1 -1
  19. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  20. package/scripts/lib/psmux-info.mjs +18 -49
  21. package/scripts/lib/skill-template.mjs +0 -26
  22. package/hub/codex-compat.mjs +0 -12
  23. package/hub/fullcycle.mjs +0 -113
  24. package/hub/gemini-adapter.mjs +0 -174
  25. package/hub/intent.mjs +0 -321
  26. package/hub/lib/mcp-response-cache.mjs +0 -239
  27. package/hub/lib/path-utils.mjs +0 -165
  28. package/hub/lib/timeline-adapter.mjs +0 -190
  29. package/hub/pipeline/gates/confidence.mjs +0 -75
  30. package/hub/pipeline/gates/consensus.mjs +0 -106
  31. package/hub/pipeline/gates/index.mjs +0 -9
  32. package/hub/pipeline/gates/selfcheck.mjs +0 -120
  33. package/hub/quality/deslop.mjs +0 -274
  34. package/hub/research.mjs +0 -155
  35. package/hub/role-control-events.mjs +0 -113
  36. package/hub/routing/complexity.mjs +0 -269
  37. package/hub/routing/index.mjs +0 -122
  38. package/hub/routing/q-learning.mjs +0 -387
  39. package/hub/token-mode.mjs +0 -228
  40. package/hud/mission-board.mjs +0 -56
  41. package/hud/providers/cto.mjs +0 -76
  42. package/scripts/lib/agent-json.mjs +0 -27
  43. package/scripts/lib/async.mjs +0 -174
  44. package/scripts/lib/cli-agy.mjs +0 -85
  45. package/scripts/lib/cli-codex.mjs +0 -62
  46. package/scripts/lib/cli-gemini.mjs +0 -37
  47. package/scripts/lib/env.mjs +0 -14
  48. package/scripts/lib/hub.mjs +0 -48
  49. package/scripts/lib/pid.mjs +0 -63
  50. package/scripts/lib/quota.mjs +0 -47
  51. package/scripts/lib/skill-state.mjs +0 -220
  52. package/scripts/lib/team.mjs +0 -75
  53. package/scripts/lib/timeout.mjs +0 -66
  54. package/scripts/lib/tmp.mjs +0 -26
  55. 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 };
@@ -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
- }
@@ -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
- }