@triflux/core 10.0.0-alpha.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/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-manager.mjs +352 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +213 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +581 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +106 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +192 -0
- package/hub/codex-adapter.mjs +190 -0
- package/hub/codex-compat.mjs +78 -0
- package/hub/codex-preflight.mjs +147 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +179 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/middleware/request-logger.mjs +81 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +245 -0
- package/hub/team-bridge.mjs +25 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +81 -0
- package/hud/hud-qos-status.mjs +206 -0
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +287 -0
- package/package.json +31 -0
- package/scripts/lib/claudemd-manager.mjs +325 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +940 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// hub/lib/process-utils.mjs
|
|
2
|
+
// 프로세스 관련 공유 유틸리티
|
|
3
|
+
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { IS_WINDOWS, killProcess } from "../platform.mjs";
|
|
9
|
+
|
|
10
|
+
const CLEANUP_SCRIPT_DIR = join(tmpdir(), "tfx-process-utils");
|
|
11
|
+
const SCAN_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "scan-processes.ps1");
|
|
12
|
+
const TREE_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "get-ancestor-tree.ps1");
|
|
13
|
+
|
|
14
|
+
// 스크립트 버전 — 내용 변경 시 증가하여 캐시된 스크립트를 갱신
|
|
15
|
+
const SCRIPT_VERSION = 3;
|
|
16
|
+
const VERSION_FILE = join(CLEANUP_SCRIPT_DIR, ".version");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 주어진 PID의 프로세스가 살아있는지 확인한다.
|
|
20
|
+
* EPERM: 프로세스는 존재하지만 signal 권한 없음 → alive
|
|
21
|
+
* ESRCH: 프로세스가 존재하지 않음 → dead
|
|
22
|
+
*/
|
|
23
|
+
export function isPidAlive(pid) {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e?.code === 'EPERM') return true;
|
|
31
|
+
if (e?.code === 'ESRCH') return false;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 동기적 sleep. Atomics.wait 우선, 불가 시 busy-wait 폴백.
|
|
38
|
+
*/
|
|
39
|
+
function sleepSyncMs(ms) {
|
|
40
|
+
try {
|
|
41
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
42
|
+
} catch {
|
|
43
|
+
const end = Date.now() + ms;
|
|
44
|
+
while (Date.now() < end) { /* spin */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 고아 PID 목록에 SIGTERM → 3초 대기 → SIGKILL 에스컬레이션을 적용한다.
|
|
50
|
+
* PID 재사용 레이스 방어: SIGTERM 전 alive 확인, SIGKILL 전 재검증.
|
|
51
|
+
* @param {number[]} orphanPids
|
|
52
|
+
* @param {Map<number, {ppid: number, name: string}>} [procMap] PID 재사용 감지용 스냅샷
|
|
53
|
+
* @returns {number} killed count
|
|
54
|
+
*/
|
|
55
|
+
function killWithEscalation(orphanPids, procMap) {
|
|
56
|
+
if (orphanPids.length === 0) return 0;
|
|
57
|
+
|
|
58
|
+
// SIGTERM 전 alive 스냅샷 — 이미 죽은 PID는 카운트에서 제외
|
|
59
|
+
const aliveBeforeKill = new Set(orphanPids.filter(pid => isPidAlive(pid)));
|
|
60
|
+
|
|
61
|
+
for (const pid of aliveBeforeKill) {
|
|
62
|
+
killProcess(pid, { signal: "SIGTERM" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sleepSyncMs(3000);
|
|
66
|
+
|
|
67
|
+
let killed = 0;
|
|
68
|
+
for (const pid of aliveBeforeKill) {
|
|
69
|
+
if (isPidAlive(pid)) {
|
|
70
|
+
// PID 재사용 방어: procMap이 있으면 스캔 시점의 ppid와 현재 ppid 비교
|
|
71
|
+
// ppid가 변경되었으면 PID가 재사용된 것이므로 kill하지 않���
|
|
72
|
+
if (procMap) {
|
|
73
|
+
const snapshot = procMap.get(pid);
|
|
74
|
+
if (snapshot) {
|
|
75
|
+
try {
|
|
76
|
+
const current = execSync(
|
|
77
|
+
IS_WINDOWS
|
|
78
|
+
? `powershell -NoProfile -WindowStyle Hidden -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}' -ErrorAction SilentlyContinue).ParentProcessId"`
|
|
79
|
+
: `ps -o ppid= -p ${pid}`,
|
|
80
|
+
{ encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
81
|
+
);
|
|
82
|
+
const currentPpid = Number.parseInt(current.trim(), 10);
|
|
83
|
+
if (Number.isFinite(currentPpid) && currentPpid !== snapshot.ppid) {
|
|
84
|
+
continue; // PID 재사용 감지 — skip
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// 조회 실패 시 안전하게 skip
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
killProcess(pid, { signal: "SIGKILL", force: true });
|
|
93
|
+
}
|
|
94
|
+
if (!isPidAlive(pid)) killed++;
|
|
95
|
+
}
|
|
96
|
+
return killed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* PowerShell 헬퍼 스크립트를 임시 디렉토리에 생성한다.
|
|
101
|
+
* bash의 $_ 이스케이핑 문제를 피하기 위해 -File로 실행.
|
|
102
|
+
*/
|
|
103
|
+
function ensureHelperScripts() {
|
|
104
|
+
mkdirSync(CLEANUP_SCRIPT_DIR, { recursive: true });
|
|
105
|
+
|
|
106
|
+
// 버전 체크 — 스크립트 갱신 필요 여부
|
|
107
|
+
let needsUpdate = true;
|
|
108
|
+
try {
|
|
109
|
+
if (existsSync(VERSION_FILE)) {
|
|
110
|
+
const cached = Number.parseInt(readFileSync(VERSION_FILE, "utf8").trim(), 10);
|
|
111
|
+
if (cached === SCRIPT_VERSION) needsUpdate = false;
|
|
112
|
+
}
|
|
113
|
+
} catch {}
|
|
114
|
+
|
|
115
|
+
if (needsUpdate) {
|
|
116
|
+
// 기존 스크립트 삭제 후 재생성
|
|
117
|
+
try { unlinkSync(SCAN_SCRIPT_PATH); } catch {}
|
|
118
|
+
try { unlinkSync(TREE_SCRIPT_PATH); } catch {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!existsSync(TREE_SCRIPT_PATH)) {
|
|
122
|
+
writeFileSync(TREE_SCRIPT_PATH, [
|
|
123
|
+
"param([int]$StartPid)",
|
|
124
|
+
"$p = $StartPid",
|
|
125
|
+
"for ($i = 0; $i -lt 10; $i++) {",
|
|
126
|
+
" if ($p -le 0) { break }",
|
|
127
|
+
" Write-Output $p",
|
|
128
|
+
' $parent = (Get-CimInstance Win32_Process -Filter "ProcessId=$p" -ErrorAction SilentlyContinue).ParentProcessId',
|
|
129
|
+
" if ($null -eq $parent -or $parent -le 0) { break }",
|
|
130
|
+
" $p = $parent",
|
|
131
|
+
"}",
|
|
132
|
+
].join("\n"), "utf8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!existsSync(SCAN_SCRIPT_PATH)) {
|
|
136
|
+
// CLI + 쉘 + 런타임 전체를 스캔하여 PID,ParentPID,Name 출력
|
|
137
|
+
// codex/claude/pwsh/uvx 누락 시 중간 프로세스가 alive 판정되어 고아 트리 전체가 보호됨
|
|
138
|
+
// 예: WT(dead)→pwsh(alive,미스캔)→codex→cmd→node — pwsh에서 isPidAlive=true로 끊김
|
|
139
|
+
writeFileSync(SCAN_SCRIPT_PATH, [
|
|
140
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
141
|
+
"Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='bash.exe' OR Name='cmd.exe' OR Name='codex.exe' OR Name='claude.exe' OR Name='pwsh.exe' OR Name='uvx.exe'\" | ForEach-Object {",
|
|
142
|
+
' Write-Output "$($_.ProcessId),$($_.ParentProcessId),$($_.Name)"',
|
|
143
|
+
"}",
|
|
144
|
+
].join("\n"), "utf8");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (needsUpdate) {
|
|
148
|
+
writeFileSync(VERSION_FILE, String(SCRIPT_VERSION), "utf8");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* PID → 루트 조상까지의 체인에서 살아있는 조상이 있는지 확인한다.
|
|
154
|
+
* 프로세스 맵을 사용하여 O(depth) 탐색.
|
|
155
|
+
* @param {number} pid
|
|
156
|
+
* @param {Map<number, {ppid: number, name: string}>} procMap
|
|
157
|
+
* @param {Set<number>} protectedPids
|
|
158
|
+
* @returns {boolean} true = 보호됨 (활성 조상 체인이 있음)
|
|
159
|
+
*/
|
|
160
|
+
function hasLiveAncestorChain(pid, procMap, protectedPids) {
|
|
161
|
+
const visited = new Set();
|
|
162
|
+
let current = pid;
|
|
163
|
+
|
|
164
|
+
while (current > 0 && !visited.has(current)) {
|
|
165
|
+
visited.add(current);
|
|
166
|
+
|
|
167
|
+
if (protectedPids.has(current)) return true;
|
|
168
|
+
|
|
169
|
+
const info = procMap.get(current);
|
|
170
|
+
if (!info) {
|
|
171
|
+
// 프로세스 맵에 없음 → 살아있는지 직접 확인
|
|
172
|
+
return isPidAlive(current);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ppid = info.ppid;
|
|
176
|
+
if (!Number.isFinite(ppid) || ppid <= 0) {
|
|
177
|
+
// 루트 프로세스 (ppid=0) — 시스템 프로세스이므로 보호
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 부모가 맵에 없고 죽었으면 → 고아 체인
|
|
182
|
+
if (!procMap.has(ppid) && !isPidAlive(ppid)) return false;
|
|
183
|
+
|
|
184
|
+
current = ppid;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// kill 대상 프로세스 이름 (Windows)
|
|
191
|
+
// codex/claude도 포함: protectedPids + hasLiveAncestorChain이 활성 인스턴스를 보호하므로
|
|
192
|
+
// 고아(부모 dead + 자식 dead)만 kill됨. pwsh.exe는 사용자 인터랙티브 쉘이므로 제외.
|
|
193
|
+
const KILLABLE_NAMES = new Set([
|
|
194
|
+
"node.exe", "bash.exe", "cmd.exe", "uvx.exe",
|
|
195
|
+
"codex.exe", "claude.exe",
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 고아 프로세스 트리를 정리한다 (node.exe + bash.exe + cmd.exe + uvx.exe).
|
|
200
|
+
* Windows 전용 — Agent 서브프로세스가 MCP 서버, bash 래퍼, cmd 래퍼를 남기는 문제 대응.
|
|
201
|
+
*
|
|
202
|
+
* 전략: 부모 체인을 루트까지 추적하여, 체인 중간에 죽은 프로세스가 있으면
|
|
203
|
+
* 해당 프로세스 아래의 전체 트리를 고아로 판정하고 정리.
|
|
204
|
+
* 스캔 범위에는 codex/claude/pwsh도 포함하여 체인 추적 정확도를 높인다.
|
|
205
|
+
*
|
|
206
|
+
* 보호 대상: 현재 프로세스 조상 트리, Hub PID
|
|
207
|
+
* @returns {{ killed: number, remaining: number }}
|
|
208
|
+
*/
|
|
209
|
+
export function cleanupOrphanNodeProcesses() {
|
|
210
|
+
if (!IS_WINDOWS) return cleanupOrphansUnix();
|
|
211
|
+
|
|
212
|
+
ensureHelperScripts();
|
|
213
|
+
|
|
214
|
+
const myPid = process.pid;
|
|
215
|
+
|
|
216
|
+
// Hub PID 보호
|
|
217
|
+
let hubPid = null;
|
|
218
|
+
try {
|
|
219
|
+
const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
220
|
+
if (existsSync(hubPidPath)) {
|
|
221
|
+
const hubInfo = JSON.parse(readFileSync(hubPidPath, "utf8"));
|
|
222
|
+
hubPid = Number(hubInfo?.pid);
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
|
|
226
|
+
// 보호 PID 세트: 현재 프로세스 + Hub + 현재 프로세스의 조상 트리
|
|
227
|
+
const protectedPids = new Set();
|
|
228
|
+
protectedPids.add(myPid);
|
|
229
|
+
if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const treeOutput = execSync(
|
|
233
|
+
`powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${TREE_SCRIPT_PATH}" -StartPid ${myPid}`,
|
|
234
|
+
{ encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
235
|
+
);
|
|
236
|
+
for (const line of treeOutput.split(/\r?\n/)) {
|
|
237
|
+
const pid = Number.parseInt(line.trim(), 10);
|
|
238
|
+
if (Number.isFinite(pid) && pid > 0) protectedPids.add(pid);
|
|
239
|
+
}
|
|
240
|
+
} catch {}
|
|
241
|
+
|
|
242
|
+
// 전체 프로세스 맵 구축 (node + bash + cmd + codex + claude + pwsh + uvx)
|
|
243
|
+
const procMap = new Map();
|
|
244
|
+
try {
|
|
245
|
+
const output = execSync(
|
|
246
|
+
`powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "${SCAN_SCRIPT_PATH}"`,
|
|
247
|
+
{ encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
for (const line of output.split(/\r?\n/)) {
|
|
251
|
+
const trimmed = line.trim();
|
|
252
|
+
if (!trimmed) continue;
|
|
253
|
+
const [pidStr, ppidStr, name] = trimmed.split(",");
|
|
254
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
255
|
+
const ppid = Number.parseInt(ppidStr, 10);
|
|
256
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
257
|
+
procMap.set(pid, { ppid, name: name || "unknown" });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch {}
|
|
261
|
+
|
|
262
|
+
// 고아 판정 + 정리 (SIGTERM → 3s → SIGKILL 에스컬레이션)
|
|
263
|
+
// CLI 도구(codex/claude/pwsh)는 체인 추적용으로만 스캔 — kill 대상에서 제외
|
|
264
|
+
const orphanPids = [];
|
|
265
|
+
for (const [pid, info] of procMap) {
|
|
266
|
+
if (protectedPids.has(pid)) continue;
|
|
267
|
+
if (!KILLABLE_NAMES.has(info.name?.toLowerCase())) continue;
|
|
268
|
+
if (hasLiveAncestorChain(pid, procMap, protectedPids)) continue;
|
|
269
|
+
orphanPids.push(pid);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const killed = killWithEscalation(orphanPids, procMap);
|
|
273
|
+
|
|
274
|
+
// 남은 프로세스 수 확인
|
|
275
|
+
let remaining = 0;
|
|
276
|
+
try {
|
|
277
|
+
const countOutput = execSync(
|
|
278
|
+
`powershell -NoProfile -WindowStyle Hidden -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
|
|
279
|
+
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
280
|
+
);
|
|
281
|
+
remaining = Number.parseInt(countOutput.trim(), 10) || 0;
|
|
282
|
+
} catch {}
|
|
283
|
+
|
|
284
|
+
return { killed, remaining };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Unix/macOS 고아 프로세스 정리.
|
|
289
|
+
* `ps -eo pid,ppid,comm` 기반 프로세스 맵 → 동일한 조상 체인 판정 → SIGKILL 에스컬레이션.
|
|
290
|
+
* @returns {{ killed: number, remaining: number }}
|
|
291
|
+
*/
|
|
292
|
+
function cleanupOrphansUnix() {
|
|
293
|
+
const myPid = process.pid;
|
|
294
|
+
|
|
295
|
+
// Hub PID 보호
|
|
296
|
+
const protectedPids = new Set();
|
|
297
|
+
protectedPids.add(myPid);
|
|
298
|
+
try {
|
|
299
|
+
const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
300
|
+
if (existsSync(hubPidPath)) {
|
|
301
|
+
const hubPid = Number(JSON.parse(readFileSync(hubPidPath, "utf8"))?.pid);
|
|
302
|
+
if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
|
|
303
|
+
}
|
|
304
|
+
} catch {}
|
|
305
|
+
|
|
306
|
+
// 현재 프로세스의 조상 트리 보호
|
|
307
|
+
try {
|
|
308
|
+
let current = myPid;
|
|
309
|
+
for (let i = 0; i < 10; i++) {
|
|
310
|
+
protectedPids.add(current);
|
|
311
|
+
const output = execSync(`ps -o ppid= -p ${current}`, {
|
|
312
|
+
encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
313
|
+
});
|
|
314
|
+
const ppid = Number.parseInt(output.trim(), 10);
|
|
315
|
+
if (!Number.isFinite(ppid) || ppid <= 1) break;
|
|
316
|
+
current = ppid;
|
|
317
|
+
}
|
|
318
|
+
} catch {}
|
|
319
|
+
|
|
320
|
+
// 프로세스 맵 구축 (런타임 + CLI — 체인 추적 정확도를 위해 CLI도 포함)
|
|
321
|
+
const procMap = new Map();
|
|
322
|
+
try {
|
|
323
|
+
const output = execSync("ps -eo pid,ppid,comm", {
|
|
324
|
+
encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
|
|
325
|
+
});
|
|
326
|
+
for (const line of output.split("\n").slice(1)) {
|
|
327
|
+
const parts = line.trim().split(/\s+/);
|
|
328
|
+
if (parts.length < 3) continue;
|
|
329
|
+
const pid = Number.parseInt(parts[0], 10);
|
|
330
|
+
const ppid = Number.parseInt(parts[1], 10);
|
|
331
|
+
const name = parts.slice(2).join(" ");
|
|
332
|
+
if (Number.isFinite(pid) && pid > 0 && /^(node|bash|sh|python|codex|claude|uvx)/.test(name)) {
|
|
333
|
+
procMap.set(pid, { ppid, name });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch {}
|
|
337
|
+
|
|
338
|
+
// kill 대상: node, python, codex, claude, uvx — bash/sh는 사용자 인터랙티브 쉘 가능성
|
|
339
|
+
const killableUnix = /^(node|python|codex|claude|uvx)/;
|
|
340
|
+
|
|
341
|
+
// 고아 판정 + SIGKILL 에스컬레이션
|
|
342
|
+
const orphanPids = [];
|
|
343
|
+
for (const [pid, info] of procMap) {
|
|
344
|
+
if (protectedPids.has(pid)) continue;
|
|
345
|
+
if (!killableUnix.test(info.name)) continue;
|
|
346
|
+
if (hasLiveAncestorChain(pid, procMap, protectedPids)) continue;
|
|
347
|
+
orphanPids.push(pid);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const killed = killWithEscalation(orphanPids, procMap);
|
|
351
|
+
|
|
352
|
+
let remaining = 0;
|
|
353
|
+
try {
|
|
354
|
+
const output = execSync("ps -eo comm | grep -c '^node$'", {
|
|
355
|
+
encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
356
|
+
});
|
|
357
|
+
remaining = Number.parseInt(output.trim(), 10) || 0;
|
|
358
|
+
} catch {}
|
|
359
|
+
|
|
360
|
+
return { killed, remaining };
|
|
361
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub HTTP 서버 요청 로깅 미들웨어.
|
|
3
|
+
*
|
|
4
|
+
* raw http.createServer에 맞춘 래퍼. Express가 아닌 triflux Hub 전용.
|
|
5
|
+
*
|
|
6
|
+
* 사용법 (server.mjs에서):
|
|
7
|
+
* import { wrapRequestHandler } from './middleware/request-logger.mjs';
|
|
8
|
+
* const httpServer = createHttpServer(wrapRequestHandler(originalHandler));
|
|
9
|
+
*
|
|
10
|
+
* 각 요청에 correlationId를 할당하고, 응답 완료 시 구조화 로그를 남긴다.
|
|
11
|
+
* health/status 체크는 로깅을 건너뛴다.
|
|
12
|
+
*/
|
|
13
|
+
import { withRequestContext, getCorrelationId } from '../../scripts/lib/context.mjs';
|
|
14
|
+
import { createModuleLogger } from '../../scripts/lib/logger.mjs';
|
|
15
|
+
|
|
16
|
+
const log = createModuleLogger('hub');
|
|
17
|
+
|
|
18
|
+
const SKIP_PATHS = new Set(['/health', '/healthz', '/status', '/ready']);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 원본 request handler를 래핑하여 로깅 + 컨텍스트 전파를 추가한다.
|
|
22
|
+
*
|
|
23
|
+
* @param {function(import('http').IncomingMessage, import('http').ServerResponse): void} handler
|
|
24
|
+
* @returns {function(import('http').IncomingMessage, import('http').ServerResponse): void}
|
|
25
|
+
*/
|
|
26
|
+
export function wrapRequestHandler(handler) {
|
|
27
|
+
return (req, res) => {
|
|
28
|
+
const path = getRequestPath(req.url);
|
|
29
|
+
|
|
30
|
+
if (SKIP_PATHS.has(path)) {
|
|
31
|
+
return handler(req, res);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const correlationId =
|
|
35
|
+
req.headers['x-correlation-id'] ||
|
|
36
|
+
req.headers['x-request-id'] ||
|
|
37
|
+
undefined; // withRequestContext will generate one
|
|
38
|
+
|
|
39
|
+
withRequestContext(
|
|
40
|
+
{
|
|
41
|
+
correlationId,
|
|
42
|
+
method: req.method,
|
|
43
|
+
path,
|
|
44
|
+
},
|
|
45
|
+
() => {
|
|
46
|
+
const startTime = process.hrtime.bigint();
|
|
47
|
+
|
|
48
|
+
// 응답 헤더에 상관 ID 포함
|
|
49
|
+
const cid = getCorrelationId();
|
|
50
|
+
if (cid) res.setHeader('X-Correlation-ID', cid);
|
|
51
|
+
|
|
52
|
+
// 응답 완료 시 로깅
|
|
53
|
+
res.on('finish', () => {
|
|
54
|
+
const duration = Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
55
|
+
const level = res.statusCode >= 500 ? 'error'
|
|
56
|
+
: res.statusCode >= 400 ? 'warn'
|
|
57
|
+
: 'info';
|
|
58
|
+
|
|
59
|
+
log[level](
|
|
60
|
+
{
|
|
61
|
+
status: res.statusCode,
|
|
62
|
+
duration: Math.round(duration * 100) / 100,
|
|
63
|
+
contentLength: res.getHeader('content-length') || 0,
|
|
64
|
+
},
|
|
65
|
+
'http.response',
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
handler(req, res);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getRequestPath(url = '/') {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(url, 'http://127.0.0.1').pathname;
|
|
78
|
+
} catch {
|
|
79
|
+
return String(url).replace(/\?.*/, '') || '/';
|
|
80
|
+
}
|
|
81
|
+
}
|
package/hub/paths.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// hub/paths.mjs — triflux 워킹 디렉토리 경로 상수
|
|
2
|
+
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const TFX_WORK_DIR = '.tfx';
|
|
7
|
+
export const TFX_PLANS_DIR = join(TFX_WORK_DIR, 'plans');
|
|
8
|
+
export const TFX_REPORTS_DIR = join(TFX_WORK_DIR, 'reports');
|
|
9
|
+
export const TFX_HANDOFFS_DIR = join(TFX_WORK_DIR, 'handoffs');
|
|
10
|
+
export const TFX_LOGS_DIR = join(TFX_WORK_DIR, 'logs');
|
|
11
|
+
export const TFX_STATE_DIR = join(TFX_WORK_DIR, 'state');
|
|
12
|
+
export const TFX_FULLCYCLE_DIR = join(TFX_WORK_DIR, 'fullcycle');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* triflux 워킹 디렉토리 구조를 보장한다.
|
|
16
|
+
* @param {string} baseDir
|
|
17
|
+
*/
|
|
18
|
+
export function ensureTfxDirs(baseDir) {
|
|
19
|
+
for (const relativeDir of [
|
|
20
|
+
TFX_WORK_DIR,
|
|
21
|
+
TFX_PLANS_DIR,
|
|
22
|
+
TFX_REPORTS_DIR,
|
|
23
|
+
TFX_HANDOFFS_DIR,
|
|
24
|
+
TFX_LOGS_DIR,
|
|
25
|
+
TFX_STATE_DIR,
|
|
26
|
+
TFX_FULLCYCLE_DIR,
|
|
27
|
+
]) {
|
|
28
|
+
mkdirSync(join(baseDir, relativeDir), { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// hub/pipeline/gates/confidence.mjs — Pre-Execution Confidence Gate
|
|
2
|
+
//
|
|
3
|
+
// plan → prd → [confidence] → exec
|
|
4
|
+
// 5단계 확신도 검증: >=90% proceed / 70-89% alternative / <70% abort
|
|
5
|
+
|
|
6
|
+
export const CRITERIA = [
|
|
7
|
+
{ id: 'no_duplicate', label: '중복 구현 없는지?', weight: 0.25 },
|
|
8
|
+
{ id: 'architecture', label: '아키텍처 준수?', weight: 0.25 },
|
|
9
|
+
{ id: 'docs_verified', label: '공식 문서 확인?', weight: 0.20 },
|
|
10
|
+
{ id: 'oss_reference', label: 'OSS 레퍼런스?', weight: 0.15 },
|
|
11
|
+
{ id: 'root_cause', label: '근본 원인 파악?', weight: 0.15 },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 확신도 검증 실행
|
|
16
|
+
* @param {string|object} planArtifact - plan 단계에서 생성된 구현 계획
|
|
17
|
+
* @param {object} context - { checks?, codebaseFiles?, existingTests? }
|
|
18
|
+
* @param {object} [context.checks] - 각 기준별 점수 (boolean 또는 0-1 숫자)
|
|
19
|
+
* @returns {{ score: number, breakdown: Array, decision: string, reasoning: string }}
|
|
20
|
+
*/
|
|
21
|
+
export function runConfidenceCheck(planArtifact, context = {}) {
|
|
22
|
+
if (!planArtifact) {
|
|
23
|
+
return {
|
|
24
|
+
score: 0,
|
|
25
|
+
breakdown: CRITERIA.map(c => ({ id: c.id, label: c.label, weight: c.weight, score: 0, passed: false })),
|
|
26
|
+
decision: 'abort',
|
|
27
|
+
reasoning: 'planArtifact가 제공되지 않았습니다.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const checks = context.checks || {};
|
|
32
|
+
|
|
33
|
+
const breakdown = CRITERIA.map(c => {
|
|
34
|
+
const raw = checks[c.id];
|
|
35
|
+
const score = typeof raw === 'number' ? Math.max(0, Math.min(1, raw)) : (raw ? 1 : 0);
|
|
36
|
+
return { id: c.id, label: c.label, weight: c.weight, score, passed: score >= 0.7 };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const totalScore = Math.round(
|
|
40
|
+
breakdown.reduce((sum, b) => sum + b.score * b.weight, 0) * 100,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
let decision, reasoning;
|
|
44
|
+
if (totalScore >= 90) {
|
|
45
|
+
decision = 'proceed';
|
|
46
|
+
reasoning = `확신도 ${totalScore}%: 모든 기준 충족. 실행 진행.`;
|
|
47
|
+
} else if (totalScore >= 70) {
|
|
48
|
+
decision = 'alternative';
|
|
49
|
+
reasoning = `확신도 ${totalScore}%: 일부 기준 미달. 대안 검토 필요.`;
|
|
50
|
+
} else {
|
|
51
|
+
decision = 'abort';
|
|
52
|
+
reasoning = `확신도 ${totalScore}%: 기준 미달. 실행 중단.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { score: totalScore, breakdown, decision, reasoning, needsReview: decision === 'alternative' };
|
|
56
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// hub/pipeline/gates/consensus.mjs — Consensus Quality Gate
|
|
2
|
+
//
|
|
3
|
+
// N개 결과의 합의도를 평가하여 5단계 분기 결정
|
|
4
|
+
// proceed(>=90%) / proceed_warn(>=75%) / retry(<75%+재시도) / escalate(<75%+감독) / abort
|
|
5
|
+
|
|
6
|
+
/** 단계별 합의 임계값 (%) */
|
|
7
|
+
export const STAGE_THRESHOLDS = {
|
|
8
|
+
plan: 50,
|
|
9
|
+
define: 75,
|
|
10
|
+
execute: 75,
|
|
11
|
+
verify: 80,
|
|
12
|
+
security: 100,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** 환경변수 기반 기본 임계값 (기본 75) */
|
|
16
|
+
function getDefaultThreshold() {
|
|
17
|
+
const env = typeof process !== 'undefined' && process.env?.TRIFLUX_CONSENSUS_THRESHOLD;
|
|
18
|
+
if (env != null && env !== '') {
|
|
19
|
+
const parsed = Number(env);
|
|
20
|
+
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 100) return parsed;
|
|
21
|
+
}
|
|
22
|
+
return 75;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 성공률 기반 5단계 분기 결정
|
|
27
|
+
* @param {number} successRate - 합의 성공률 (0-100)
|
|
28
|
+
* @param {number} retryCount - 현재 재시도 횟수
|
|
29
|
+
* @param {number} maxRetries - 최대 재시도 횟수
|
|
30
|
+
* @param {string} [mode] - 실행 모드 ('supervised' | 기타)
|
|
31
|
+
* @returns {'proceed' | 'proceed_warn' | 'retry' | 'escalate' | 'abort'}
|
|
32
|
+
*/
|
|
33
|
+
export function evaluateQualityBranch(successRate, retryCount, maxRetries, mode) {
|
|
34
|
+
if (successRate >= 90) return 'proceed';
|
|
35
|
+
if (successRate >= 75) return 'proceed_warn';
|
|
36
|
+
|
|
37
|
+
// <75%: 재시도 가능 여부에 따라 분기
|
|
38
|
+
if (retryCount < maxRetries) return 'retry';
|
|
39
|
+
if (mode === 'supervised') return 'escalate';
|
|
40
|
+
return 'abort';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* N개 결과의 합의도 평가
|
|
45
|
+
* @param {Array<{ success: boolean }>} results - 평가 대상 결과 배열
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @param {string} [options.stage] - 파이프라인 단계 (STAGE_THRESHOLDS 키)
|
|
48
|
+
* @param {number} [options.threshold] - 합의 임계값 직접 지정 (stage보다 우선)
|
|
49
|
+
* @param {number} [options.retryCount=0] - 현재 재시도 횟수
|
|
50
|
+
* @param {number} [options.maxRetries=2] - 최대 재시도 횟수
|
|
51
|
+
* @param {string} [options.mode] - 실행 모드 ('supervised' 등)
|
|
52
|
+
* @returns {{ successRate: number, threshold: number, decision: string, reasoning: string, results: Array }}
|
|
53
|
+
*/
|
|
54
|
+
export function evaluateConsensus(results, options = {}) {
|
|
55
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
successRate: 0,
|
|
58
|
+
threshold: options.threshold ?? getDefaultThreshold(),
|
|
59
|
+
decision: 'abort',
|
|
60
|
+
reasoning: '평가 대상 결과가 없습니다.',
|
|
61
|
+
results: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const retryCount = options.retryCount ?? 0;
|
|
66
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
67
|
+
const mode = options.mode;
|
|
68
|
+
|
|
69
|
+
// 임계값 결정: 직접 지정 > stage별 > 환경변수 > 기본 75
|
|
70
|
+
const threshold = options.threshold
|
|
71
|
+
?? (options.stage && STAGE_THRESHOLDS[options.stage])
|
|
72
|
+
?? getDefaultThreshold();
|
|
73
|
+
|
|
74
|
+
const successCount = results.filter(r => r.success).length;
|
|
75
|
+
const successRate = Math.round((successCount / results.length) * 100);
|
|
76
|
+
|
|
77
|
+
const decision = evaluateQualityBranch(successRate, retryCount, maxRetries, mode);
|
|
78
|
+
|
|
79
|
+
const reasoningMap = {
|
|
80
|
+
proceed: `합의율 ${successRate}% (>= 90%): 전원 합의. 진행.`,
|
|
81
|
+
proceed_warn: `합의율 ${successRate}% (>= 75%): 부분 합의. 경고와 함께 진행.`,
|
|
82
|
+
retry: `합의율 ${successRate}% (< 75%): 재시도 ${retryCount + 1}/${maxRetries} 가능.`,
|
|
83
|
+
escalate: `합의율 ${successRate}% (< 75%): 감독 모드 에스컬레이션.`,
|
|
84
|
+
abort: `합의율 ${successRate}% (< 75%): 합의 실패. 중단.`,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
successRate,
|
|
89
|
+
threshold,
|
|
90
|
+
decision,
|
|
91
|
+
reasoning: reasoningMap[decision],
|
|
92
|
+
results,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// hub/pipeline/gates/index.mjs — Quality Gates re-export
|
|
2
|
+
|
|
3
|
+
export { CRITERIA, runConfidenceCheck } from './confidence.mjs';
|
|
4
|
+
export { RED_FLAGS, QUESTIONS, runSelfCheck } from './selfcheck.mjs';
|
|
5
|
+
export { STAGE_THRESHOLDS, evaluateQualityBranch, evaluateConsensus } from './consensus.mjs';
|