@triflux/core 10.51.0 → 10.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hub/bridge.mjs +69 -1197
- package/hub/cli-adapter-base.mjs +4 -104
- package/hub/codex-adapter.mjs +7 -116
- package/hub/index.mjs +3 -20
- package/hub/lib/process-utils.mjs +1 -33
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/claude-transcript.mjs +19 -0
- package/hub/team/nativeProxy.mjs +759 -0
- package/hub/team/session-context.mjs +1 -1
- package/hud/constants.mjs +9 -22
- package/hud/hud-qos-status.mjs +16 -19
- package/hud/providers/codex-probe.mjs +2 -31
- package/hud/providers/codex.mjs +16 -58
- package/hud/providers/gemini.mjs +20 -0
- package/hud/renderers.mjs +101 -77
- package/hud/utils.mjs +9 -13
- package/package.json +3 -6
- package/scripts/lib/agent-route-policy.mjs +0 -67
- package/scripts/lib/cli-claude.mjs +0 -12
- package/scripts/lib/env-probe.mjs +0 -70
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/mcp-guard-engine.mjs +42 -101
- package/scripts/lib/session-presence.mjs +1 -99
- package/hub/account-broker.mjs +0 -1221
- package/hub/adaptive-diagnostic.mjs +0 -350
- package/hub/adaptive-inject.mjs +0 -224
- package/hub/adaptive-memory.mjs +0 -277
- package/hub/adaptive.mjs +0 -172
- package/hub/assign-callbacks.mjs +0 -170
- package/hub/delegator/contracts.mjs +0 -38
- package/hub/delegator/index.mjs +0 -14
- package/hub/delegator/schema/delegator-tools.schema.json +0 -250
- package/hub/delegator/service.mjs +0 -336
- package/hub/delegator/tool-definitions.mjs +0 -35
- package/hub/hitl.mjs +0 -202
- package/hub/lib/known-errors.json +0 -72
- package/hub/lib/memory-store.mjs +0 -1001
- package/hub/lib/phase-manager.mjs +0 -493
- package/hub/lib/state-snapshot.mjs +0 -293
- package/hub/lib/trace-recorder.mjs +0 -153
- package/hub/lib/uuidv7.mjs +0 -45
- package/hub/memory-doctor.mjs +0 -640
- package/hub/middleware/quota-middleware.mjs +0 -70
- package/hub/middleware/request-logger.mjs +0 -191
- package/hub/paths.mjs +0 -24
- package/hub/pipeline/index.mjs +0 -163
- package/hub/pipeline/state.mjs +0 -191
- package/hub/pipeline/transitions.mjs +0 -131
- package/hub/reflexion.mjs +0 -290
- package/hub/role-contract.mjs +0 -312
- package/hub/router.mjs +0 -2026
- package/hub/session-fingerprint.mjs +0 -418
- package/hub/state.mjs +0 -300
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team-bridge.mjs +0 -27
- package/scripts/lib/claudemd-scanner.mjs +0 -26
- package/scripts/lib/context.mjs +0 -51
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,191 +0,0 @@
|
|
|
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
|
-
|
|
14
|
-
import { HUB_REQUEST_MONITOR_CACHE_PATH } from "../../hud/constants.mjs";
|
|
15
|
-
import { createContextMonitor } from "../../hud/context-monitor.mjs";
|
|
16
|
-
import {
|
|
17
|
-
getCorrelationId,
|
|
18
|
-
withRequestContext,
|
|
19
|
-
} from "../../scripts/lib/context.mjs";
|
|
20
|
-
import { createModuleLogger } from "../../scripts/lib/logger.mjs";
|
|
21
|
-
|
|
22
|
-
const log = createModuleLogger("hub");
|
|
23
|
-
const contextMonitor = createContextMonitor({
|
|
24
|
-
cachePath: HUB_REQUEST_MONITOR_CACHE_PATH,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const SKIP_PATHS = new Set(["/health", "/healthz", "/status", "/ready"]);
|
|
28
|
-
const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 원본 request handler를 래핑하여 로깅 + 컨텍스트 전파를 추가한다.
|
|
32
|
-
*
|
|
33
|
-
* @param {function(import('http').IncomingMessage, import('http').ServerResponse): void} handler
|
|
34
|
-
* @returns {function(import('http').IncomingMessage, import('http').ServerResponse): void}
|
|
35
|
-
*/
|
|
36
|
-
export function wrapRequestHandler(handler) {
|
|
37
|
-
return (req, res) => {
|
|
38
|
-
const path = getRequestPath(req.url);
|
|
39
|
-
|
|
40
|
-
if (SKIP_PATHS.has(path)) {
|
|
41
|
-
return handler(req, res);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const correlationId =
|
|
45
|
-
req.headers["x-correlation-id"] ||
|
|
46
|
-
req.headers["x-request-id"] ||
|
|
47
|
-
undefined; // withRequestContext will generate one
|
|
48
|
-
|
|
49
|
-
withRequestContext(
|
|
50
|
-
{
|
|
51
|
-
correlationId,
|
|
52
|
-
method: req.method,
|
|
53
|
-
path,
|
|
54
|
-
},
|
|
55
|
-
() => {
|
|
56
|
-
const startTime = process.hrtime.bigint();
|
|
57
|
-
const reqChunks = [];
|
|
58
|
-
let reqBytes = 0;
|
|
59
|
-
let reqOverflow = false;
|
|
60
|
-
|
|
61
|
-
req.on("data", (chunk) => {
|
|
62
|
-
if (reqOverflow) return;
|
|
63
|
-
const buf = Buffer.isBuffer(chunk)
|
|
64
|
-
? chunk
|
|
65
|
-
: Buffer.from(String(chunk));
|
|
66
|
-
reqBytes += buf.length;
|
|
67
|
-
if (reqBytes > MAX_CAPTURE_BYTES) {
|
|
68
|
-
reqOverflow = true;
|
|
69
|
-
reqChunks.length = 0;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
reqChunks.push(buf);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const resChunks = [];
|
|
76
|
-
let resBytes = 0;
|
|
77
|
-
let resOverflow = false;
|
|
78
|
-
|
|
79
|
-
const originalWrite = res.write.bind(res);
|
|
80
|
-
const originalEnd = res.end.bind(res);
|
|
81
|
-
|
|
82
|
-
function captureResponseChunk(chunk) {
|
|
83
|
-
if (resOverflow || chunk == null) return;
|
|
84
|
-
const buf = Buffer.isBuffer(chunk)
|
|
85
|
-
? chunk
|
|
86
|
-
: Buffer.from(String(chunk));
|
|
87
|
-
resBytes += buf.length;
|
|
88
|
-
if (resBytes > MAX_CAPTURE_BYTES) {
|
|
89
|
-
resOverflow = true;
|
|
90
|
-
resChunks.length = 0;
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
resChunks.push(buf);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
res.write = function writePatched(chunk, ...args) {
|
|
97
|
-
captureResponseChunk(chunk);
|
|
98
|
-
return originalWrite(chunk, ...args);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
res.end = function endPatched(chunk, ...args) {
|
|
102
|
-
captureResponseChunk(chunk);
|
|
103
|
-
return originalEnd(chunk, ...args);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// 응답 헤더에 상관 ID 포함
|
|
107
|
-
const cid = getCorrelationId();
|
|
108
|
-
if (cid) res.setHeader("X-Correlation-ID", cid);
|
|
109
|
-
|
|
110
|
-
// 응답 완료 시 로깅
|
|
111
|
-
res.on("finish", () => {
|
|
112
|
-
const duration =
|
|
113
|
-
Number(process.hrtime.bigint() - startTime) / 1_000_000;
|
|
114
|
-
const level =
|
|
115
|
-
res.statusCode >= 500
|
|
116
|
-
? "error"
|
|
117
|
-
: res.statusCode >= 400
|
|
118
|
-
? "warn"
|
|
119
|
-
: "info";
|
|
120
|
-
const reqBodyText = reqOverflow
|
|
121
|
-
? ""
|
|
122
|
-
: Buffer.concat(reqChunks).toString("utf8");
|
|
123
|
-
const resBodyText = resOverflow
|
|
124
|
-
? ""
|
|
125
|
-
: Buffer.concat(resChunks).toString("utf8");
|
|
126
|
-
const tokenSummary = contextMonitor.record({
|
|
127
|
-
requestBody: reqBodyText,
|
|
128
|
-
requestBytes:
|
|
129
|
-
reqBytes || Number(req.headers["content-length"] || 0),
|
|
130
|
-
responseBody: resBodyText,
|
|
131
|
-
responseBytes:
|
|
132
|
-
resBytes || Number(res.getHeader("content-length") || 0),
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
log[level](
|
|
136
|
-
{
|
|
137
|
-
status: res.statusCode,
|
|
138
|
-
duration: Math.round(duration * 100) / 100,
|
|
139
|
-
contentLength: res.getHeader("content-length") || 0,
|
|
140
|
-
tokenUsage: {
|
|
141
|
-
request: tokenSummary.requestTokens,
|
|
142
|
-
response: tokenSummary.responseTokens,
|
|
143
|
-
total: tokenSummary.totalTokens,
|
|
144
|
-
context: tokenSummary.display,
|
|
145
|
-
warningLevel: tokenSummary.warningLevel,
|
|
146
|
-
overheadMs: tokenSummary.overheadMs,
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
"http.response",
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
if (tokenSummary.warningLevel === "critical") {
|
|
153
|
-
log.error(
|
|
154
|
-
{
|
|
155
|
-
context: tokenSummary.display,
|
|
156
|
-
message: tokenSummary.warningMessage,
|
|
157
|
-
},
|
|
158
|
-
"context.critical",
|
|
159
|
-
);
|
|
160
|
-
} else if (tokenSummary.warningLevel === "warn") {
|
|
161
|
-
log.warn(
|
|
162
|
-
{
|
|
163
|
-
context: tokenSummary.display,
|
|
164
|
-
message: tokenSummary.warningMessage,
|
|
165
|
-
},
|
|
166
|
-
"context.warn",
|
|
167
|
-
);
|
|
168
|
-
} else if (tokenSummary.warningLevel === "info") {
|
|
169
|
-
log.info(
|
|
170
|
-
{
|
|
171
|
-
context: tokenSummary.display,
|
|
172
|
-
message: tokenSummary.warningMessage,
|
|
173
|
-
},
|
|
174
|
-
"context.info",
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
handler(req, res);
|
|
180
|
-
},
|
|
181
|
-
);
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getRequestPath(url = "/") {
|
|
186
|
-
try {
|
|
187
|
-
return new URL(url, "http://127.0.0.1").pathname;
|
|
188
|
-
} catch {
|
|
189
|
-
return String(url).replace(/\?.*/, "") || "/";
|
|
190
|
-
}
|
|
191
|
-
}
|
package/hub/paths.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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_STATE_DIR = join(TFX_WORK_DIR, "state");
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* triflux 워킹 디렉토리 구조를 보장한다.
|
|
13
|
-
* @param {string} baseDir
|
|
14
|
-
*/
|
|
15
|
-
export function ensureTfxDirs(baseDir) {
|
|
16
|
-
for (const relativeDir of [
|
|
17
|
-
TFX_WORK_DIR,
|
|
18
|
-
TFX_PLANS_DIR,
|
|
19
|
-
TFX_REPORTS_DIR,
|
|
20
|
-
TFX_STATE_DIR,
|
|
21
|
-
]) {
|
|
22
|
-
mkdirSync(join(baseDir, relativeDir), { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
}
|
package/hub/pipeline/index.mjs
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// hub/pipeline/index.mjs — 파이프라인 매니저
|
|
2
|
-
//
|
|
3
|
-
// 상태(state.mjs) + 전이(transitions.mjs) 통합 인터페이스
|
|
4
|
-
|
|
5
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { join, resolve } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
ensurePipelineTable,
|
|
9
|
-
initPipelineState,
|
|
10
|
-
readPipelineState,
|
|
11
|
-
removePipelineState,
|
|
12
|
-
updatePipelineState,
|
|
13
|
-
} from "./state.mjs";
|
|
14
|
-
import {
|
|
15
|
-
canTransition,
|
|
16
|
-
ralphRestart,
|
|
17
|
-
TERMINAL,
|
|
18
|
-
transitionPhase,
|
|
19
|
-
} from "./transitions.mjs";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 파이프라인 매니저 생성
|
|
23
|
-
* @param {object} db - better-sqlite3 인스턴스 (store.db)
|
|
24
|
-
* @param {string} teamName
|
|
25
|
-
* @param {object} opts - { fix_max?, ralph_max? }
|
|
26
|
-
* @returns {object} 파이프라인 API
|
|
27
|
-
*/
|
|
28
|
-
export function createPipeline(db, teamName, opts = {}) {
|
|
29
|
-
ensurePipelineTable(db);
|
|
30
|
-
|
|
31
|
-
// 기존 상태가 있으면 로드, 없으면 초기화
|
|
32
|
-
let state = readPipelineState(db, teamName);
|
|
33
|
-
if (!state) {
|
|
34
|
-
state = initPipelineState(db, teamName, opts);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
/**
|
|
39
|
-
* 현재 상태 조회
|
|
40
|
-
*/
|
|
41
|
-
getState() {
|
|
42
|
-
state = readPipelineState(db, teamName) || state;
|
|
43
|
-
return { ...state };
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 다음 단계로 전이 가능 여부
|
|
48
|
-
* @param {string} phase
|
|
49
|
-
*/
|
|
50
|
-
canAdvance(phase) {
|
|
51
|
-
const current = readPipelineState(db, teamName);
|
|
52
|
-
return current ? canTransition(current.phase, phase) : false;
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 다음 단계로 전이
|
|
57
|
-
* @param {string} nextPhase
|
|
58
|
-
* @returns {{ ok: boolean, state?: object, error?: string }}
|
|
59
|
-
*/
|
|
60
|
-
advance(nextPhase) {
|
|
61
|
-
const current = readPipelineState(db, teamName);
|
|
62
|
-
if (!current) {
|
|
63
|
-
return { ok: false, error: `파이프라인 없음: ${teamName}` };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const result = transitionPhase(current, nextPhase);
|
|
67
|
-
if (!result.ok) return result;
|
|
68
|
-
|
|
69
|
-
state = updatePipelineState(db, teamName, result.state);
|
|
70
|
-
return { ok: true, state: { ...state } };
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* ralph loop 재시작 (plan부터 다시)
|
|
75
|
-
* @returns {{ ok: boolean, state?: object, error?: string }}
|
|
76
|
-
*/
|
|
77
|
-
restart() {
|
|
78
|
-
const current = readPipelineState(db, teamName);
|
|
79
|
-
if (!current) {
|
|
80
|
-
return { ok: false, error: `파이프라인 없음: ${teamName}` };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const result = ralphRestart(current);
|
|
84
|
-
if (!result.ok) return result;
|
|
85
|
-
|
|
86
|
-
state = updatePipelineState(db, teamName, result.state);
|
|
87
|
-
return { ok: true, state: { ...state } };
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* DAG 컨텍스트를 파이프라인 상태에 저장
|
|
92
|
-
* @param {{ dag_width: number, levels: Record<number, string[]>, edges: Array<{from:string, to:string}>, max_complexity: string, taskResults: Record<string, *> }} dagContext
|
|
93
|
-
*/
|
|
94
|
-
setDagContext(dagContext) {
|
|
95
|
-
const current = readPipelineState(db, teamName);
|
|
96
|
-
if (!current) return;
|
|
97
|
-
const artifacts = { ...(current.artifacts || {}), dagContext };
|
|
98
|
-
state = updatePipelineState(db, teamName, { artifacts });
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* DAG 컨텍스트 조회 (편의 메서드)
|
|
103
|
-
* @returns {{ dag_width: number, levels: Record<number, string[]>, edges: Array<{from:string, to:string}>, max_complexity: string, taskResults: Record<string, *> } | null}
|
|
104
|
-
*/
|
|
105
|
-
getDagContext() {
|
|
106
|
-
const current = readPipelineState(db, teamName) || state;
|
|
107
|
-
return current?.artifacts?.dagContext || null;
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* artifact 저장 (plan_path, prd_path, verify_report 등)
|
|
112
|
-
* @param {string} key
|
|
113
|
-
* @param {*} value
|
|
114
|
-
*/
|
|
115
|
-
setArtifact(key, value) {
|
|
116
|
-
const current = readPipelineState(db, teamName);
|
|
117
|
-
if (!current) return;
|
|
118
|
-
const artifacts = { ...(current.artifacts || {}), [key]: value };
|
|
119
|
-
state = updatePipelineState(db, teamName, { artifacts });
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Plan 파일을 .tfx/plans/{teamName}-plan.md 에 기록하고
|
|
124
|
-
* artifact('plan_path')에 절대 경로를 저장한다.
|
|
125
|
-
* @param {string} content - Plan markdown 내용
|
|
126
|
-
* @returns {string} 절대 경로
|
|
127
|
-
*/
|
|
128
|
-
writePlanFile(content) {
|
|
129
|
-
const safeName = teamName.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_");
|
|
130
|
-
const planDir = resolve(process.cwd(), ".tfx", "plans");
|
|
131
|
-
mkdirSync(planDir, { recursive: true });
|
|
132
|
-
const planPath = join(planDir, `${safeName}-plan.md`);
|
|
133
|
-
writeFileSync(planPath, content, "utf8");
|
|
134
|
-
this.setArtifact("plan_path", planPath);
|
|
135
|
-
return planPath;
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* 터미널 상태 여부
|
|
140
|
-
*/
|
|
141
|
-
isTerminal() {
|
|
142
|
-
const current = readPipelineState(db, teamName);
|
|
143
|
-
return current ? TERMINAL.has(current.phase) : true;
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 파이프라인 초기화 (리셋)
|
|
148
|
-
*/
|
|
149
|
-
reset() {
|
|
150
|
-
state = initPipelineState(db, teamName, opts);
|
|
151
|
-
return { ...state };
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 파이프라인 삭제
|
|
156
|
-
*/
|
|
157
|
-
remove() {
|
|
158
|
-
return removePipelineState(db, teamName);
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
export { ensurePipelineTable } from "./state.mjs";
|
|
163
|
-
export { ALLOWED, canTransition, PHASES, TERMINAL } from "./transitions.mjs";
|
package/hub/pipeline/state.mjs
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
// hub/pipeline/state.mjs — Hub SQLite 파이프라인 상태 저장/로드
|
|
2
|
-
//
|
|
3
|
-
// store.mjs의 기존 SQLite 연결(db)을 활용한다.
|
|
4
|
-
// pipeline_state 테이블은 schema.sql에 정의.
|
|
5
|
-
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
import { ensureTfxDirs, TFX_STATE_DIR } from "../paths.mjs";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 파이프라인 상태 DB 경로를 계산한다.
|
|
12
|
-
* @param {string} baseDir
|
|
13
|
-
* @returns {string}
|
|
14
|
-
*/
|
|
15
|
-
export function getPipelineStateDbPath(baseDir) {
|
|
16
|
-
return join(baseDir, TFX_STATE_DIR, "state.db");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 파이프라인 상태 DB 경로와 .tfx 디렉토리를 준비한다.
|
|
21
|
-
* @param {string} baseDir
|
|
22
|
-
* @returns {string}
|
|
23
|
-
*/
|
|
24
|
-
export function ensurePipelineStateDbPath(baseDir) {
|
|
25
|
-
ensureTfxDirs(baseDir);
|
|
26
|
-
return getPipelineStateDbPath(baseDir);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* pipeline_state 테이블 초기화 (store.db에 없으면 생성)
|
|
31
|
-
* @param {object} db - better-sqlite3 인스턴스
|
|
32
|
-
*/
|
|
33
|
-
export function ensurePipelineTable(db) {
|
|
34
|
-
db.exec(`
|
|
35
|
-
CREATE TABLE IF NOT EXISTS pipeline_state (
|
|
36
|
-
team_name TEXT PRIMARY KEY,
|
|
37
|
-
phase TEXT NOT NULL DEFAULT 'plan',
|
|
38
|
-
fix_attempt INTEGER DEFAULT 0,
|
|
39
|
-
fix_max INTEGER DEFAULT 3,
|
|
40
|
-
ralph_iteration INTEGER DEFAULT 0,
|
|
41
|
-
ralph_max INTEGER DEFAULT 10,
|
|
42
|
-
artifacts TEXT DEFAULT '{}',
|
|
43
|
-
phase_history TEXT DEFAULT '[]',
|
|
44
|
-
created_at INTEGER,
|
|
45
|
-
updated_at INTEGER
|
|
46
|
-
)
|
|
47
|
-
`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const STATEMENTS = new WeakMap();
|
|
51
|
-
|
|
52
|
-
function getStatements(db) {
|
|
53
|
-
let s = STATEMENTS.get(db);
|
|
54
|
-
if (s) return s;
|
|
55
|
-
|
|
56
|
-
s = {
|
|
57
|
-
get: db.prepare("SELECT * FROM pipeline_state WHERE team_name = ?"),
|
|
58
|
-
insert: db.prepare(`
|
|
59
|
-
INSERT INTO pipeline_state (team_name, phase, fix_attempt, fix_max, ralph_iteration, ralph_max, artifacts, phase_history, created_at, updated_at)
|
|
60
|
-
VALUES (@team_name, @phase, @fix_attempt, @fix_max, @ralph_iteration, @ralph_max, @artifacts, @phase_history, @created_at, @updated_at)
|
|
61
|
-
`),
|
|
62
|
-
update: db.prepare(`
|
|
63
|
-
UPDATE pipeline_state SET
|
|
64
|
-
phase = @phase,
|
|
65
|
-
fix_attempt = @fix_attempt,
|
|
66
|
-
fix_max = @fix_max,
|
|
67
|
-
ralph_iteration = @ralph_iteration,
|
|
68
|
-
ralph_max = @ralph_max,
|
|
69
|
-
artifacts = @artifacts,
|
|
70
|
-
phase_history = @phase_history,
|
|
71
|
-
updated_at = @updated_at
|
|
72
|
-
WHERE team_name = @team_name
|
|
73
|
-
`),
|
|
74
|
-
remove: db.prepare("DELETE FROM pipeline_state WHERE team_name = ?"),
|
|
75
|
-
list: db.prepare("SELECT * FROM pipeline_state ORDER BY updated_at DESC"),
|
|
76
|
-
};
|
|
77
|
-
STATEMENTS.set(db, s);
|
|
78
|
-
return s;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function parseRow(row) {
|
|
82
|
-
if (!row) return null;
|
|
83
|
-
return {
|
|
84
|
-
...row,
|
|
85
|
-
artifacts: JSON.parse(row.artifacts || "{}"),
|
|
86
|
-
phase_history: JSON.parse(row.phase_history || "[]"),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function serializeState(state) {
|
|
91
|
-
return {
|
|
92
|
-
team_name: state.team_name,
|
|
93
|
-
phase: state.phase || "plan",
|
|
94
|
-
fix_attempt: state.fix_attempt ?? 0,
|
|
95
|
-
fix_max: state.fix_max ?? 3,
|
|
96
|
-
ralph_iteration: state.ralph_iteration ?? 0,
|
|
97
|
-
ralph_max: state.ralph_max ?? 10,
|
|
98
|
-
artifacts: JSON.stringify(state.artifacts || {}),
|
|
99
|
-
phase_history: JSON.stringify(state.phase_history || []),
|
|
100
|
-
created_at: state.created_at ?? Date.now(),
|
|
101
|
-
updated_at: state.updated_at ?? Date.now(),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 파이프라인 상태 초기화 (새 파이프라인)
|
|
107
|
-
* @param {object} db - better-sqlite3 인스턴스
|
|
108
|
-
* @param {string} teamName
|
|
109
|
-
* @param {object} opts - { fix_max?, ralph_max? }
|
|
110
|
-
* @returns {object} 초기 상태
|
|
111
|
-
*/
|
|
112
|
-
export function initPipelineState(db, teamName, opts = {}) {
|
|
113
|
-
return db.transaction(() => {
|
|
114
|
-
const S = getStatements(db);
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
const state = {
|
|
117
|
-
team_name: teamName,
|
|
118
|
-
phase: "plan",
|
|
119
|
-
fix_attempt: 0,
|
|
120
|
-
fix_max: opts.fix_max ?? 3,
|
|
121
|
-
ralph_iteration: 0,
|
|
122
|
-
ralph_max: opts.ralph_max ?? 10,
|
|
123
|
-
artifacts: {},
|
|
124
|
-
phase_history: [],
|
|
125
|
-
created_at: now,
|
|
126
|
-
updated_at: now,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// 기존 상태가 있으면 삭제 후 재생성
|
|
130
|
-
S.remove.run(teamName);
|
|
131
|
-
S.insert.run(serializeState(state));
|
|
132
|
-
return state;
|
|
133
|
-
})();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* 파이프라인 상태 조회
|
|
138
|
-
* @param {object} db - better-sqlite3 인스턴스
|
|
139
|
-
* @param {string} teamName
|
|
140
|
-
* @returns {object|null}
|
|
141
|
-
*/
|
|
142
|
-
export function readPipelineState(db, teamName) {
|
|
143
|
-
const S = getStatements(db);
|
|
144
|
-
return parseRow(S.get.get(teamName));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* 파이프라인 상태 업데이트 (부분 패치)
|
|
149
|
-
* @param {object} db - better-sqlite3 인스턴스
|
|
150
|
-
* @param {string} teamName
|
|
151
|
-
* @param {object} patch - 업데이트할 필드
|
|
152
|
-
* @returns {object|null} 업데이트된 상태
|
|
153
|
-
*/
|
|
154
|
-
export function updatePipelineState(db, teamName, patch) {
|
|
155
|
-
return db.transaction(() => {
|
|
156
|
-
const S = getStatements(db);
|
|
157
|
-
const current = parseRow(S.get.get(teamName));
|
|
158
|
-
if (!current) return null;
|
|
159
|
-
|
|
160
|
-
const merged = {
|
|
161
|
-
...current,
|
|
162
|
-
...patch,
|
|
163
|
-
team_name: teamName, // team_name 변경 불가
|
|
164
|
-
updated_at: Date.now(),
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
S.update.run(serializeState(merged));
|
|
168
|
-
return merged;
|
|
169
|
-
})();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* 파이프라인 상태 삭제
|
|
174
|
-
* @param {object} db - better-sqlite3 인스턴스
|
|
175
|
-
* @param {string} teamName
|
|
176
|
-
* @returns {boolean}
|
|
177
|
-
*/
|
|
178
|
-
export function removePipelineState(db, teamName) {
|
|
179
|
-
const S = getStatements(db);
|
|
180
|
-
return S.remove.run(teamName).changes > 0;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* 활성 파이프라인 목록
|
|
185
|
-
* @param {object} db - better-sqlite3 인스턴스
|
|
186
|
-
* @returns {object[]}
|
|
187
|
-
*/
|
|
188
|
-
export function listPipelineStates(db) {
|
|
189
|
-
const S = getStatements(db);
|
|
190
|
-
return S.list.all().map(parseRow);
|
|
191
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// hub/pipeline/transitions.mjs — 파이프라인 단계 전이 규칙
|
|
2
|
-
//
|
|
3
|
-
// plan → prd → confidence → exec → deslop → verify → selfcheck → complete/fix
|
|
4
|
-
// fix → exec/verify/complete/failed
|
|
5
|
-
// complete, failed = 터미널 상태
|
|
6
|
-
|
|
7
|
-
export const PHASES = [
|
|
8
|
-
"plan",
|
|
9
|
-
"prd",
|
|
10
|
-
"confidence",
|
|
11
|
-
"exec",
|
|
12
|
-
"deslop",
|
|
13
|
-
"verify",
|
|
14
|
-
"selfcheck",
|
|
15
|
-
"fix",
|
|
16
|
-
"complete",
|
|
17
|
-
"failed",
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
export const TERMINAL = new Set(["complete", "failed"]);
|
|
21
|
-
|
|
22
|
-
export const ALLOWED = {
|
|
23
|
-
plan: ["prd"],
|
|
24
|
-
prd: ["confidence"],
|
|
25
|
-
confidence: ["exec", "failed"],
|
|
26
|
-
exec: ["deslop"],
|
|
27
|
-
deslop: ["verify"],
|
|
28
|
-
verify: ["selfcheck", "fix", "failed"],
|
|
29
|
-
selfcheck: ["complete", "fix"],
|
|
30
|
-
fix: ["exec", "verify", "complete", "failed"],
|
|
31
|
-
complete: [],
|
|
32
|
-
failed: [],
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 전이 가능 여부 확인
|
|
37
|
-
* @param {string} from - 현재 단계
|
|
38
|
-
* @param {string} to - 다음 단계
|
|
39
|
-
* @returns {boolean}
|
|
40
|
-
*/
|
|
41
|
-
export function canTransition(from, to) {
|
|
42
|
-
const targets = ALLOWED[from];
|
|
43
|
-
if (!targets) return false;
|
|
44
|
-
return targets.includes(to);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 상태 전이 실행 — fix loop 바운딩 포함
|
|
49
|
-
* @param {object} state - 파이프라인 상태 객체
|
|
50
|
-
* @param {string} nextPhase - 다음 단계
|
|
51
|
-
* @returns {{ ok: boolean, state?: object, error?: string }}
|
|
52
|
-
*/
|
|
53
|
-
export function transitionPhase(state, nextPhase) {
|
|
54
|
-
const current = state.phase;
|
|
55
|
-
|
|
56
|
-
if (!canTransition(current, nextPhase)) {
|
|
57
|
-
return {
|
|
58
|
-
ok: false,
|
|
59
|
-
error: `전이 불가: ${current} → ${nextPhase}. 허용: [${(ALLOWED[current] || []).join(", ")}]`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const next = { ...state, phase: nextPhase, updated_at: Date.now() };
|
|
64
|
-
|
|
65
|
-
// fix 단계 진입 시 attempt 증가 + 바운딩
|
|
66
|
-
if (nextPhase === "fix") {
|
|
67
|
-
next.fix_attempt = (state.fix_attempt || 0) + 1;
|
|
68
|
-
if (next.fix_attempt > (state.fix_max || 3)) {
|
|
69
|
-
return {
|
|
70
|
-
ok: false,
|
|
71
|
-
error: `fix loop 초과: ${state.fix_max || 3}회 도달`,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// verify → fix → ... → verify 반복 후 fix_max 초과 시 ralph loop
|
|
77
|
-
if (nextPhase === "failed" && current === "fix") {
|
|
78
|
-
next.ralph_iteration = (state.ralph_iteration || 0) + 1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// phase_history 기록
|
|
82
|
-
const history = Array.isArray(state.phase_history)
|
|
83
|
-
? [...state.phase_history]
|
|
84
|
-
: [];
|
|
85
|
-
history.push({ from: current, to: nextPhase, at: Date.now() });
|
|
86
|
-
next.phase_history = history;
|
|
87
|
-
|
|
88
|
-
return { ok: true, state: next };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* ralph loop 재시작 전이
|
|
93
|
-
* fix_max 초과 시 plan으로 돌아가며 ralph_iteration 증가
|
|
94
|
-
* @param {object} state - 현재 상태
|
|
95
|
-
* @returns {{ ok: boolean, state?: object, error?: string }}
|
|
96
|
-
*/
|
|
97
|
-
export function ralphRestart(state) {
|
|
98
|
-
if (TERMINAL.has(state.phase)) {
|
|
99
|
-
return { ok: false, error: "터미널 상태에서 재시작 불가" };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const iteration = (state.ralph_iteration || 0) + 1;
|
|
103
|
-
if (iteration > (state.ralph_max || 10)) {
|
|
104
|
-
return {
|
|
105
|
-
ok: false,
|
|
106
|
-
error: `ralph loop 초과: ${iteration}/${state.ralph_max || 10}회. 최종 실패.`,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const history = Array.isArray(state.phase_history)
|
|
111
|
-
? [...state.phase_history]
|
|
112
|
-
: [];
|
|
113
|
-
history.push({
|
|
114
|
-
from: state.phase,
|
|
115
|
-
to: "plan",
|
|
116
|
-
at: Date.now(),
|
|
117
|
-
ralph_restart: true,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
ok: true,
|
|
122
|
-
state: {
|
|
123
|
-
...state,
|
|
124
|
-
phase: "plan",
|
|
125
|
-
fix_attempt: 0,
|
|
126
|
-
ralph_iteration: iteration,
|
|
127
|
-
phase_history: history,
|
|
128
|
-
updated_at: Date.now(),
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|