@triflux/core 10.1.0 → 10.25.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 +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const FAILURE_STATUSES = new Set(["quota_hit", "error"]);
|
|
2
|
+
|
|
3
|
+
function buildFailureRecord(result = {}) {
|
|
4
|
+
const failure = {
|
|
5
|
+
id: result.id ?? "unknown",
|
|
6
|
+
status: result.status ?? "unknown",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
if (Number.isFinite(result.http)) {
|
|
10
|
+
failure.http = result.http;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof result.message === "string" && result.message.length > 0) {
|
|
14
|
+
failure.message = result.message;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (result.headers && Object.keys(result.headers).length > 0) {
|
|
18
|
+
failure.headers = result.headers;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return failure;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function summarizeQuotaResults(results = []) {
|
|
25
|
+
const metrics = {
|
|
26
|
+
checked: results.length,
|
|
27
|
+
ok: 0,
|
|
28
|
+
quotaHit: 0,
|
|
29
|
+
error: 0,
|
|
30
|
+
failed: 0,
|
|
31
|
+
};
|
|
32
|
+
const failures = [];
|
|
33
|
+
|
|
34
|
+
for (const result of results) {
|
|
35
|
+
const status = result?.status ?? "unknown";
|
|
36
|
+
|
|
37
|
+
if (status === "ok") {
|
|
38
|
+
metrics.ok += 1;
|
|
39
|
+
} else if (status === "quota_hit") {
|
|
40
|
+
metrics.quotaHit += 1;
|
|
41
|
+
} else if (status === "error") {
|
|
42
|
+
metrics.error += 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (FAILURE_STATUSES.has(status)) {
|
|
46
|
+
failures.push(buildFailureRecord(result));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
metrics.failed = failures.length;
|
|
51
|
+
return { metrics, failures };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function logQuotaRefreshFailures(logger, results = []) {
|
|
55
|
+
const { metrics, failures } = summarizeQuotaResults(results);
|
|
56
|
+
if (failures.length === 0) {
|
|
57
|
+
return { logged: false, metrics, failures };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.warn(
|
|
61
|
+
{
|
|
62
|
+
tag: "hub-quota",
|
|
63
|
+
metrics,
|
|
64
|
+
failures,
|
|
65
|
+
},
|
|
66
|
+
"broker.quota_refresh_degraded",
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return { logged: true, metrics, failures };
|
|
70
|
+
}
|
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
* 각 요청에 correlationId를 할당하고, 응답 완료 시 구조화 로그를 남긴다.
|
|
11
11
|
* health/status 체크는 로깅을 건너뛴다.
|
|
12
12
|
*/
|
|
13
|
-
import { withRequestContext, getCorrelationId } from '../../scripts/lib/context.mjs';
|
|
14
|
-
import { createModuleLogger } from '../../scripts/lib/logger.mjs';
|
|
15
|
-
import { createContextMonitor } from '../../hud/context-monitor.mjs';
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
import { createContextMonitor } from "../../hud/context-monitor.mjs";
|
|
15
|
+
import {
|
|
16
|
+
getCorrelationId,
|
|
17
|
+
withRequestContext,
|
|
18
|
+
} from "../../scripts/lib/context.mjs";
|
|
19
|
+
import { createModuleLogger } from "../../scripts/lib/logger.mjs";
|
|
20
|
+
|
|
21
|
+
const log = createModuleLogger("hub");
|
|
18
22
|
const contextMonitor = createContextMonitor();
|
|
19
23
|
|
|
20
|
-
const SKIP_PATHS = new Set([
|
|
24
|
+
const SKIP_PATHS = new Set(["/health", "/healthz", "/status", "/ready"]);
|
|
21
25
|
const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -35,8 +39,8 @@ export function wrapRequestHandler(handler) {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const correlationId =
|
|
38
|
-
req.headers[
|
|
39
|
-
req.headers[
|
|
42
|
+
req.headers["x-correlation-id"] ||
|
|
43
|
+
req.headers["x-request-id"] ||
|
|
40
44
|
undefined; // withRequestContext will generate one
|
|
41
45
|
|
|
42
46
|
withRequestContext(
|
|
@@ -51,9 +55,11 @@ export function wrapRequestHandler(handler) {
|
|
|
51
55
|
let reqBytes = 0;
|
|
52
56
|
let reqOverflow = false;
|
|
53
57
|
|
|
54
|
-
req.on(
|
|
58
|
+
req.on("data", (chunk) => {
|
|
55
59
|
if (reqOverflow) return;
|
|
56
|
-
const buf = Buffer.isBuffer(chunk)
|
|
60
|
+
const buf = Buffer.isBuffer(chunk)
|
|
61
|
+
? chunk
|
|
62
|
+
: Buffer.from(String(chunk));
|
|
57
63
|
reqBytes += buf.length;
|
|
58
64
|
if (reqBytes > MAX_CAPTURE_BYTES) {
|
|
59
65
|
reqOverflow = true;
|
|
@@ -72,7 +78,9 @@ export function wrapRequestHandler(handler) {
|
|
|
72
78
|
|
|
73
79
|
function captureResponseChunk(chunk) {
|
|
74
80
|
if (resOverflow || chunk == null) return;
|
|
75
|
-
const buf = Buffer.isBuffer(chunk)
|
|
81
|
+
const buf = Buffer.isBuffer(chunk)
|
|
82
|
+
? chunk
|
|
83
|
+
: Buffer.from(String(chunk));
|
|
76
84
|
resBytes += buf.length;
|
|
77
85
|
if (resBytes > MAX_CAPTURE_BYTES) {
|
|
78
86
|
resOverflow = true;
|
|
@@ -94,28 +102,38 @@ export function wrapRequestHandler(handler) {
|
|
|
94
102
|
|
|
95
103
|
// 응답 헤더에 상관 ID 포함
|
|
96
104
|
const cid = getCorrelationId();
|
|
97
|
-
if (cid) res.setHeader(
|
|
105
|
+
if (cid) res.setHeader("X-Correlation-ID", cid);
|
|
98
106
|
|
|
99
107
|
// 응답 완료 시 로깅
|
|
100
|
-
res.on(
|
|
101
|
-
const duration =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
res.on("finish", () => {
|
|
109
|
+
const duration =
|
|
110
|
+
Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
111
|
+
const level =
|
|
112
|
+
res.statusCode >= 500
|
|
113
|
+
? "error"
|
|
114
|
+
: res.statusCode >= 400
|
|
115
|
+
? "warn"
|
|
116
|
+
: "info";
|
|
117
|
+
const reqBodyText = reqOverflow
|
|
118
|
+
? ""
|
|
119
|
+
: Buffer.concat(reqChunks).toString("utf8");
|
|
120
|
+
const resBodyText = resOverflow
|
|
121
|
+
? ""
|
|
122
|
+
: Buffer.concat(resChunks).toString("utf8");
|
|
107
123
|
const tokenSummary = contextMonitor.record({
|
|
108
124
|
requestBody: reqBodyText,
|
|
109
|
-
requestBytes:
|
|
125
|
+
requestBytes:
|
|
126
|
+
reqBytes || Number(req.headers["content-length"] || 0),
|
|
110
127
|
responseBody: resBodyText,
|
|
111
|
-
responseBytes:
|
|
128
|
+
responseBytes:
|
|
129
|
+
resBytes || Number(res.getHeader("content-length") || 0),
|
|
112
130
|
});
|
|
113
131
|
|
|
114
132
|
log[level](
|
|
115
133
|
{
|
|
116
134
|
status: res.statusCode,
|
|
117
135
|
duration: Math.round(duration * 100) / 100,
|
|
118
|
-
contentLength: res.getHeader(
|
|
136
|
+
contentLength: res.getHeader("content-length") || 0,
|
|
119
137
|
tokenUsage: {
|
|
120
138
|
request: tokenSummary.requestTokens,
|
|
121
139
|
response: tokenSummary.responseTokens,
|
|
@@ -125,23 +143,32 @@ export function wrapRequestHandler(handler) {
|
|
|
125
143
|
overheadMs: tokenSummary.overheadMs,
|
|
126
144
|
},
|
|
127
145
|
},
|
|
128
|
-
|
|
146
|
+
"http.response",
|
|
129
147
|
);
|
|
130
148
|
|
|
131
|
-
if (tokenSummary.warningLevel ===
|
|
149
|
+
if (tokenSummary.warningLevel === "critical") {
|
|
132
150
|
log.error(
|
|
133
|
-
{
|
|
134
|
-
|
|
151
|
+
{
|
|
152
|
+
context: tokenSummary.display,
|
|
153
|
+
message: tokenSummary.warningMessage,
|
|
154
|
+
},
|
|
155
|
+
"context.critical",
|
|
135
156
|
);
|
|
136
|
-
} else if (tokenSummary.warningLevel ===
|
|
157
|
+
} else if (tokenSummary.warningLevel === "warn") {
|
|
137
158
|
log.warn(
|
|
138
|
-
{
|
|
139
|
-
|
|
159
|
+
{
|
|
160
|
+
context: tokenSummary.display,
|
|
161
|
+
message: tokenSummary.warningMessage,
|
|
162
|
+
},
|
|
163
|
+
"context.warn",
|
|
140
164
|
);
|
|
141
|
-
} else if (tokenSummary.warningLevel ===
|
|
165
|
+
} else if (tokenSummary.warningLevel === "info") {
|
|
142
166
|
log.info(
|
|
143
|
-
{
|
|
144
|
-
|
|
167
|
+
{
|
|
168
|
+
context: tokenSummary.display,
|
|
169
|
+
message: tokenSummary.warningMessage,
|
|
170
|
+
},
|
|
171
|
+
"context.info",
|
|
145
172
|
);
|
|
146
173
|
}
|
|
147
174
|
});
|
|
@@ -152,10 +179,10 @@ export function wrapRequestHandler(handler) {
|
|
|
152
179
|
};
|
|
153
180
|
}
|
|
154
181
|
|
|
155
|
-
function getRequestPath(url =
|
|
182
|
+
function getRequestPath(url = "/") {
|
|
156
183
|
try {
|
|
157
|
-
return new URL(url,
|
|
184
|
+
return new URL(url, "http://127.0.0.1").pathname;
|
|
158
185
|
} catch {
|
|
159
|
-
return String(url).replace(/\?.*/,
|
|
186
|
+
return String(url).replace(/\?.*/, "") || "/";
|
|
160
187
|
}
|
|
161
188
|
}
|
package/hub/paths.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// hub/paths.mjs — triflux 워킹 디렉토리 경로 상수
|
|
2
2
|
|
|
3
|
-
import { mkdirSync } from
|
|
4
|
-
import { join } from
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
export const TFX_WORK_DIR =
|
|
7
|
-
export const TFX_PLANS_DIR = join(TFX_WORK_DIR,
|
|
8
|
-
export const TFX_REPORTS_DIR = join(TFX_WORK_DIR,
|
|
9
|
-
export const TFX_HANDOFFS_DIR = join(TFX_WORK_DIR,
|
|
10
|
-
export const TFX_LOGS_DIR = join(TFX_WORK_DIR,
|
|
11
|
-
export const TFX_STATE_DIR = join(TFX_WORK_DIR,
|
|
12
|
-
export const TFX_FULLCYCLE_DIR = join(TFX_WORK_DIR,
|
|
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
13
|
|
|
14
14
|
/**
|
|
15
15
|
* triflux 워킹 디렉토리 구조를 보장한다.
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// 5단계 확신도 검증: >=90% proceed / 70-89% alternative / <70% abort
|
|
5
5
|
|
|
6
6
|
export const CRITERIA = [
|
|
7
|
-
{ id:
|
|
8
|
-
{ id:
|
|
9
|
-
{ id:
|
|
10
|
-
{ id:
|
|
11
|
-
{ id:
|
|
7
|
+
{ id: "no_duplicate", label: "중복 구현 없는지?", weight: 0.25 },
|
|
8
|
+
{ id: "architecture", label: "아키텍처 준수?", weight: 0.25 },
|
|
9
|
+
{ id: "docs_verified", label: "공식 문서 확인?", weight: 0.2 },
|
|
10
|
+
{ id: "oss_reference", label: "OSS 레퍼런스?", weight: 0.15 },
|
|
11
|
+
{ id: "root_cause", label: "근본 원인 파악?", weight: 0.15 },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -22,18 +22,31 @@ export function runConfidenceCheck(planArtifact, context = {}) {
|
|
|
22
22
|
if (!planArtifact) {
|
|
23
23
|
return {
|
|
24
24
|
score: 0,
|
|
25
|
-
breakdown: CRITERIA.map(c => ({
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
breakdown: CRITERIA.map((c) => ({
|
|
26
|
+
id: c.id,
|
|
27
|
+
label: c.label,
|
|
28
|
+
weight: c.weight,
|
|
29
|
+
score: 0,
|
|
30
|
+
passed: false,
|
|
31
|
+
})),
|
|
32
|
+
decision: "abort",
|
|
33
|
+
reasoning: "planArtifact가 제공되지 않았습니다.",
|
|
28
34
|
};
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const checks = context.checks || {};
|
|
32
38
|
|
|
33
|
-
const breakdown = CRITERIA.map(c => {
|
|
39
|
+
const breakdown = CRITERIA.map((c) => {
|
|
34
40
|
const raw = checks[c.id];
|
|
35
|
-
const score =
|
|
36
|
-
|
|
41
|
+
const score =
|
|
42
|
+
typeof raw === "number" ? Math.max(0, Math.min(1, raw)) : raw ? 1 : 0;
|
|
43
|
+
return {
|
|
44
|
+
id: c.id,
|
|
45
|
+
label: c.label,
|
|
46
|
+
weight: c.weight,
|
|
47
|
+
score,
|
|
48
|
+
passed: score >= 0.7,
|
|
49
|
+
};
|
|
37
50
|
});
|
|
38
51
|
|
|
39
52
|
const totalScore = Math.round(
|
|
@@ -42,15 +55,21 @@ export function runConfidenceCheck(planArtifact, context = {}) {
|
|
|
42
55
|
|
|
43
56
|
let decision, reasoning;
|
|
44
57
|
if (totalScore >= 90) {
|
|
45
|
-
decision =
|
|
58
|
+
decision = "proceed";
|
|
46
59
|
reasoning = `확신도 ${totalScore}%: 모든 기준 충족. 실행 진행.`;
|
|
47
60
|
} else if (totalScore >= 70) {
|
|
48
|
-
decision =
|
|
61
|
+
decision = "alternative";
|
|
49
62
|
reasoning = `확신도 ${totalScore}%: 일부 기준 미달. 대안 검토 필요.`;
|
|
50
63
|
} else {
|
|
51
|
-
decision =
|
|
64
|
+
decision = "abort";
|
|
52
65
|
reasoning = `확신도 ${totalScore}%: 기준 미달. 실행 중단.`;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
score: totalScore,
|
|
70
|
+
breakdown,
|
|
71
|
+
decision,
|
|
72
|
+
reasoning,
|
|
73
|
+
needsReview: decision === "alternative",
|
|
74
|
+
};
|
|
56
75
|
}
|
|
@@ -14,8 +14,9 @@ export const STAGE_THRESHOLDS = {
|
|
|
14
14
|
|
|
15
15
|
/** 환경변수 기반 기본 임계값 (기본 75) */
|
|
16
16
|
function getDefaultThreshold() {
|
|
17
|
-
const env =
|
|
18
|
-
|
|
17
|
+
const env =
|
|
18
|
+
typeof process !== "undefined" && process.env?.TRIFLUX_CONSENSUS_THRESHOLD;
|
|
19
|
+
if (env != null && env !== "") {
|
|
19
20
|
const parsed = Number(env);
|
|
20
21
|
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 100) return parsed;
|
|
21
22
|
}
|
|
@@ -30,14 +31,19 @@ function getDefaultThreshold() {
|
|
|
30
31
|
* @param {string} [mode] - 실행 모드 ('supervised' | 기타)
|
|
31
32
|
* @returns {'proceed' | 'proceed_warn' | 'retry' | 'escalate' | 'abort'}
|
|
32
33
|
*/
|
|
33
|
-
export function evaluateQualityBranch(
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
export function evaluateQualityBranch(
|
|
35
|
+
successRate,
|
|
36
|
+
retryCount,
|
|
37
|
+
maxRetries,
|
|
38
|
+
mode,
|
|
39
|
+
) {
|
|
40
|
+
if (successRate >= 90) return "proceed";
|
|
41
|
+
if (successRate >= 75) return "proceed_warn";
|
|
36
42
|
|
|
37
43
|
// <75%: 재시도 가능 여부에 따라 분기
|
|
38
|
-
if (retryCount < maxRetries) return
|
|
39
|
-
if (mode ===
|
|
40
|
-
return
|
|
44
|
+
if (retryCount < maxRetries) return "retry";
|
|
45
|
+
if (mode === "supervised") return "escalate";
|
|
46
|
+
return "abort";
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
/**
|
|
@@ -56,8 +62,8 @@ export function evaluateConsensus(results, options = {}) {
|
|
|
56
62
|
return {
|
|
57
63
|
successRate: 0,
|
|
58
64
|
threshold: options.threshold ?? getDefaultThreshold(),
|
|
59
|
-
decision:
|
|
60
|
-
reasoning:
|
|
65
|
+
decision: "abort",
|
|
66
|
+
reasoning: "평가 대상 결과가 없습니다.",
|
|
61
67
|
results: [],
|
|
62
68
|
};
|
|
63
69
|
}
|
|
@@ -67,14 +73,20 @@ export function evaluateConsensus(results, options = {}) {
|
|
|
67
73
|
const mode = options.mode;
|
|
68
74
|
|
|
69
75
|
// 임계값 결정: 직접 지정 > stage별 > 환경변수 > 기본 75
|
|
70
|
-
const threshold =
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
const threshold =
|
|
77
|
+
options.threshold ??
|
|
78
|
+
(options.stage && STAGE_THRESHOLDS[options.stage]) ??
|
|
79
|
+
getDefaultThreshold();
|
|
73
80
|
|
|
74
|
-
const successCount = results.filter(r => r.success).length;
|
|
81
|
+
const successCount = results.filter((r) => r.success).length;
|
|
75
82
|
const successRate = Math.round((successCount / results.length) * 100);
|
|
76
83
|
|
|
77
|
-
const decision = evaluateQualityBranch(
|
|
84
|
+
const decision = evaluateQualityBranch(
|
|
85
|
+
successRate,
|
|
86
|
+
retryCount,
|
|
87
|
+
maxRetries,
|
|
88
|
+
mode,
|
|
89
|
+
);
|
|
78
90
|
|
|
79
91
|
const reasoningMap = {
|
|
80
92
|
proceed: `합의율 ${successRate}% (>= 90%): 전원 합의. 진행.`,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// hub/pipeline/gates/index.mjs — Quality Gates re-export
|
|
2
2
|
|
|
3
|
-
export { CRITERIA, runConfidenceCheck } from
|
|
4
|
-
export {
|
|
5
|
-
|
|
3
|
+
export { CRITERIA, runConfidenceCheck } from "./confidence.mjs";
|
|
4
|
+
export {
|
|
5
|
+
evaluateConsensus,
|
|
6
|
+
evaluateQualityBranch,
|
|
7
|
+
STAGE_THRESHOLDS,
|
|
8
|
+
} from "./consensus.mjs";
|
|
9
|
+
export { QUESTIONS, RED_FLAGS, runSelfCheck } from "./selfcheck.mjs";
|
|
@@ -4,20 +4,56 @@
|
|
|
4
4
|
// 4대 필수 질문 + 7대 할루시네이션 Red Flag 탐지
|
|
5
5
|
|
|
6
6
|
export const RED_FLAGS = [
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
|
|
7
|
+
{
|
|
8
|
+
id: "test_pass_no_output",
|
|
9
|
+
pattern: /테스트\s*(?:가\s*)?통과/,
|
|
10
|
+
label: '"테스트 통과" (출력 없이)',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "everything_works",
|
|
14
|
+
pattern: /모든\s*게?\s*작동/,
|
|
15
|
+
label: '"모든게 작동" (증거 없이)',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "no_changes_with_diff",
|
|
19
|
+
pattern: /변경\s*(?:사항\s*)?없/,
|
|
20
|
+
label: '"변경 없음" (diff 있는데)',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "backward_compatible",
|
|
24
|
+
pattern: /호환성\s*(?:이\s*)?유지/,
|
|
25
|
+
label: '"호환성 유지" (검증 없이)',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "performance_improved",
|
|
29
|
+
pattern: /성능\s*(?:이\s*)?개선/,
|
|
30
|
+
label: '"성능 개선" (벤치마크 없이)',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "security_enhanced",
|
|
34
|
+
pattern: /보안\s*(?:이\s*)?강화/,
|
|
35
|
+
label: '"보안 강화" (증거 없이)',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "error_handling_done",
|
|
39
|
+
pattern: /에러\s*처리\s*(?:가\s*)?완료/,
|
|
40
|
+
label: '"에러 처리 완료" (catch 블록만)',
|
|
41
|
+
},
|
|
14
42
|
];
|
|
15
43
|
|
|
16
44
|
export const QUESTIONS = [
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
{
|
|
46
|
+
id: "tests_passing",
|
|
47
|
+
label: "모든 테스트 통과?",
|
|
48
|
+
evidenceKey: "testOutput",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "requirements_met",
|
|
52
|
+
label: "모든 요구사항 충족?",
|
|
53
|
+
evidenceKey: "requirementChecklist",
|
|
54
|
+
},
|
|
55
|
+
{ id: "no_assumptions", label: "검증 없는 가정?", evidenceKey: "references" },
|
|
56
|
+
{ id: "evidence_provided", label: "증거 있는가?", evidenceKey: "artifacts" },
|
|
21
57
|
];
|
|
22
58
|
|
|
23
59
|
/**
|
|
@@ -34,10 +70,10 @@ function detectRedFlags(text, context = {}) {
|
|
|
34
70
|
if (!rf.pattern.test(text)) continue;
|
|
35
71
|
|
|
36
72
|
// "변경 없음"은 실제 diff가 있을 때만 Red Flag
|
|
37
|
-
if (rf.id ===
|
|
73
|
+
if (rf.id === "no_changes_with_diff" && !context.hasDiff) continue;
|
|
38
74
|
|
|
39
75
|
// "테스트 통과"는 testOutput 증거가 없을 때만 Red Flag
|
|
40
|
-
if (rf.id ===
|
|
76
|
+
if (rf.id === "test_pass_no_output" && evidence.testOutput) continue;
|
|
41
77
|
|
|
42
78
|
// 기타 Red Flag는 해당 id의 반증이 있으면 스킵
|
|
43
79
|
if (evidence[rf.id]) continue;
|
|
@@ -58,24 +94,26 @@ function detectRedFlags(text, context = {}) {
|
|
|
58
94
|
* @returns {{ passed: boolean, score: number, flags: Array, checklist: Array }}
|
|
59
95
|
*/
|
|
60
96
|
export function runSelfCheck(execResult, verifyResult, requirements = {}) {
|
|
61
|
-
const normalize = (v) =>
|
|
62
|
-
|
|
97
|
+
const normalize = (v) =>
|
|
98
|
+
typeof v === "string" ? v : v != null ? JSON.stringify(v) : "";
|
|
99
|
+
const text = [normalize(execResult), normalize(verifyResult)].join("\n");
|
|
63
100
|
|
|
64
101
|
const flags = detectRedFlags(text, requirements);
|
|
65
102
|
|
|
66
103
|
const evidence = requirements.evidence || {};
|
|
67
|
-
const checklist = QUESTIONS.map(q => {
|
|
104
|
+
const checklist = QUESTIONS.map((q) => {
|
|
68
105
|
const ev = evidence[q.evidenceKey];
|
|
69
|
-
const passed =
|
|
106
|
+
const passed =
|
|
107
|
+
ev != null && (typeof ev === "string" ? ev.trim().length > 0 : true);
|
|
70
108
|
return { id: q.id, label: q.label, passed, evidence: ev || null };
|
|
71
109
|
});
|
|
72
110
|
|
|
73
|
-
const allQuestionsPassed = checklist.every(q => q.passed);
|
|
111
|
+
const allQuestionsPassed = checklist.every((q) => q.passed);
|
|
74
112
|
const passed = flags.length === 0 && allQuestionsPassed;
|
|
75
113
|
|
|
76
114
|
// 점수: 기본 100, Red Flag당 -15, 실패 질문당 -20
|
|
77
115
|
const flagPenalty = flags.length * 15;
|
|
78
|
-
const questionPenalty = checklist.filter(q => !q.passed).length * 20;
|
|
116
|
+
const questionPenalty = checklist.filter((q) => !q.passed).length * 20;
|
|
79
117
|
const score = Math.max(0, 100 - flagPenalty - questionPenalty);
|
|
80
118
|
|
|
81
119
|
return { passed, score, flags, checklist };
|