@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.
Files changed (75) hide show
  1. package/hooks/agent-route-guard.mjs +109 -0
  2. package/hooks/cross-review-tracker.mjs +122 -0
  3. package/hooks/error-context.mjs +148 -0
  4. package/hooks/hook-manager.mjs +352 -0
  5. package/hooks/hook-orchestrator.mjs +312 -0
  6. package/hooks/hook-registry.json +213 -0
  7. package/hooks/hooks.json +89 -0
  8. package/hooks/keyword-rules.json +581 -0
  9. package/hooks/lib/resolve-root.mjs +59 -0
  10. package/hooks/mcp-config-watcher.mjs +85 -0
  11. package/hooks/pipeline-stop.mjs +76 -0
  12. package/hooks/safety-guard.mjs +106 -0
  13. package/hooks/subagent-verifier.mjs +80 -0
  14. package/hub/assign-callbacks.mjs +133 -0
  15. package/hub/bridge.mjs +799 -0
  16. package/hub/cli-adapter-base.mjs +192 -0
  17. package/hub/codex-adapter.mjs +190 -0
  18. package/hub/codex-compat.mjs +78 -0
  19. package/hub/codex-preflight.mjs +147 -0
  20. package/hub/delegator/contracts.mjs +37 -0
  21. package/hub/delegator/index.mjs +14 -0
  22. package/hub/delegator/schema/delegator-tools.schema.json +250 -0
  23. package/hub/delegator/service.mjs +307 -0
  24. package/hub/delegator/tool-definitions.mjs +35 -0
  25. package/hub/fullcycle.mjs +96 -0
  26. package/hub/gemini-adapter.mjs +179 -0
  27. package/hub/hitl.mjs +143 -0
  28. package/hub/intent.mjs +193 -0
  29. package/hub/lib/process-utils.mjs +361 -0
  30. package/hub/middleware/request-logger.mjs +81 -0
  31. package/hub/paths.mjs +30 -0
  32. package/hub/pipeline/gates/confidence.mjs +56 -0
  33. package/hub/pipeline/gates/consensus.mjs +94 -0
  34. package/hub/pipeline/gates/index.mjs +5 -0
  35. package/hub/pipeline/gates/selfcheck.mjs +82 -0
  36. package/hub/pipeline/index.mjs +318 -0
  37. package/hub/pipeline/state.mjs +191 -0
  38. package/hub/pipeline/transitions.mjs +124 -0
  39. package/hub/platform.mjs +225 -0
  40. package/hub/quality/deslop.mjs +253 -0
  41. package/hub/reflexion.mjs +372 -0
  42. package/hub/research.mjs +146 -0
  43. package/hub/router.mjs +791 -0
  44. package/hub/routing/complexity.mjs +166 -0
  45. package/hub/routing/index.mjs +117 -0
  46. package/hub/routing/q-learning.mjs +336 -0
  47. package/hub/session-fingerprint.mjs +352 -0
  48. package/hub/state.mjs +245 -0
  49. package/hub/team-bridge.mjs +25 -0
  50. package/hub/token-mode.mjs +224 -0
  51. package/hub/workers/worker-utils.mjs +104 -0
  52. package/hud/colors.mjs +88 -0
  53. package/hud/constants.mjs +81 -0
  54. package/hud/hud-qos-status.mjs +206 -0
  55. package/hud/providers/claude.mjs +309 -0
  56. package/hud/providers/codex.mjs +151 -0
  57. package/hud/providers/gemini.mjs +320 -0
  58. package/hud/renderers.mjs +424 -0
  59. package/hud/terminal.mjs +140 -0
  60. package/hud/utils.mjs +287 -0
  61. package/package.json +31 -0
  62. package/scripts/lib/claudemd-manager.mjs +325 -0
  63. package/scripts/lib/context.mjs +67 -0
  64. package/scripts/lib/cross-review-utils.mjs +51 -0
  65. package/scripts/lib/env-probe.mjs +241 -0
  66. package/scripts/lib/gemini-profiles.mjs +85 -0
  67. package/scripts/lib/hook-utils.mjs +14 -0
  68. package/scripts/lib/keyword-rules.mjs +166 -0
  69. package/scripts/lib/logger.mjs +105 -0
  70. package/scripts/lib/mcp-filter.mjs +739 -0
  71. package/scripts/lib/mcp-guard-engine.mjs +940 -0
  72. package/scripts/lib/mcp-manifest.mjs +79 -0
  73. package/scripts/lib/mcp-server-catalog.mjs +118 -0
  74. package/scripts/lib/psmux-info.mjs +119 -0
  75. package/scripts/lib/remote-spawn-transfer.mjs +196 -0
@@ -0,0 +1,82 @@
1
+ // hub/pipeline/gates/selfcheck.mjs — Post-Execution Self-Check (Hallucination Detection)
2
+ //
3
+ // exec → verify → [selfcheck] → complete/fix
4
+ // 4대 필수 질문 + 7대 할루시네이션 Red Flag 탐지
5
+
6
+ export const RED_FLAGS = [
7
+ { id: 'test_pass_no_output', pattern: /테스트\s*(?:가\s*)?통과/, label: '"테스트 통과" (출력 없이)' },
8
+ { id: 'everything_works', pattern: /모든\s*게?\s*작동/, label: '"모든게 작동" (증거 없이)' },
9
+ { id: 'no_changes_with_diff', pattern: /변경\s*(?:사항\s*)?없/, label: '"변경 없음" (diff 있는데)' },
10
+ { id: 'backward_compatible', pattern: /호환성\s*(?:이\s*)?유지/, label: '"호환성 유지" (검증 없이)' },
11
+ { id: 'performance_improved', pattern: /성능\s*(?:이\s*)?개선/, label: '"성능 개선" (벤치마크 없이)' },
12
+ { id: 'security_enhanced', pattern: /보안\s*(?:이\s*)?강화/, label: '"보안 강화" (증거 없이)' },
13
+ { id: 'error_handling_done', pattern: /에러\s*처리\s*(?:가\s*)?완료/, label: '"에러 처리 완료" (catch 블록만)' },
14
+ ];
15
+
16
+ export const QUESTIONS = [
17
+ { id: 'tests_passing', label: '모든 테스트 통과?', evidenceKey: 'testOutput' },
18
+ { id: 'requirements_met', label: '모든 요구사항 충족?', evidenceKey: 'requirementChecklist' },
19
+ { id: 'no_assumptions', label: '검증 없는 가정?', evidenceKey: 'references' },
20
+ { id: 'evidence_provided', label: '증거 있는가?', evidenceKey: 'artifacts' },
21
+ ];
22
+
23
+ /**
24
+ * Red Flag 스캔 — 텍스트에서 할루시네이션 패턴 탐지
25
+ * @param {string} text - 스캔 대상 텍스트
26
+ * @param {object} context - { hasDiff?, evidence? }
27
+ * @returns {Array<{ id: string, label: string }>}
28
+ */
29
+ function detectRedFlags(text, context = {}) {
30
+ const flags = [];
31
+ const evidence = context.evidence || {};
32
+
33
+ for (const rf of RED_FLAGS) {
34
+ if (!rf.pattern.test(text)) continue;
35
+
36
+ // "변경 없음"은 실제 diff가 있을 때만 Red Flag
37
+ if (rf.id === 'no_changes_with_diff' && !context.hasDiff) continue;
38
+
39
+ // "테스트 통과"는 testOutput 증거가 없을 때만 Red Flag
40
+ if (rf.id === 'test_pass_no_output' && evidence.testOutput) continue;
41
+
42
+ // 기타 Red Flag는 해당 id의 반증이 있으면 스킵
43
+ if (evidence[rf.id]) continue;
44
+
45
+ flags.push({ id: rf.id, label: rf.label });
46
+ }
47
+
48
+ return flags;
49
+ }
50
+
51
+ /**
52
+ * Self-Check 실행
53
+ * @param {string|object} execResult - 실행 결과 (텍스트 또는 객체)
54
+ * @param {string|object} verifyResult - 검증 결과 (텍스트 또는 객체)
55
+ * @param {object} requirements - { hasDiff?, evidence? }
56
+ * @param {boolean} [requirements.hasDiff] - diff 존재 여부
57
+ * @param {object} [requirements.evidence] - { testOutput, requirementChecklist, references, artifacts }
58
+ * @returns {{ passed: boolean, score: number, flags: Array, checklist: Array }}
59
+ */
60
+ export function runSelfCheck(execResult, verifyResult, requirements = {}) {
61
+ const normalize = (v) => typeof v === 'string' ? v : (v != null ? JSON.stringify(v) : '');
62
+ const text = [normalize(execResult), normalize(verifyResult)].join('\n');
63
+
64
+ const flags = detectRedFlags(text, requirements);
65
+
66
+ const evidence = requirements.evidence || {};
67
+ const checklist = QUESTIONS.map(q => {
68
+ const ev = evidence[q.evidenceKey];
69
+ const passed = ev != null && (typeof ev === 'string' ? ev.trim().length > 0 : true);
70
+ return { id: q.id, label: q.label, passed, evidence: ev || null };
71
+ });
72
+
73
+ const allQuestionsPassed = checklist.every(q => q.passed);
74
+ const passed = flags.length === 0 && allQuestionsPassed;
75
+
76
+ // 점수: 기본 100, Red Flag당 -15, 실패 질문당 -20
77
+ const flagPenalty = flags.length * 15;
78
+ const questionPenalty = checklist.filter(q => !q.passed).length * 20;
79
+ const score = Math.max(0, 100 - flagPenalty - questionPenalty);
80
+
81
+ return { passed, score, flags, checklist };
82
+ }
@@ -0,0 +1,318 @@
1
+ // hub/pipeline/index.mjs — 파이프라인 매니저
2
+ //
3
+ // 상태(state.mjs) + 전이(transitions.mjs) 통합 인터페이스
4
+
5
+ import { writeFileSync, mkdirSync } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+
9
+ import { canTransition, transitionPhase, ralphRestart, TERMINAL } from './transitions.mjs';
10
+ import {
11
+ ensurePipelineTable,
12
+ initPipelineState,
13
+ readPipelineState,
14
+ updatePipelineState,
15
+ removePipelineState,
16
+ } from './state.mjs';
17
+ import { runConfidenceCheck } from './gates/confidence.mjs';
18
+ import { runSelfCheck } from './gates/selfcheck.mjs';
19
+ import { classifyIntent as _classifyIntent } from '../intent.mjs';
20
+ // deslop gate: 호출자가 scanDirectory/detectSlop 결과를 전달
21
+
22
+ /**
23
+ * 파이프라인 매니저 생성
24
+ * @param {object} db - better-sqlite3 인스턴스 (store.db)
25
+ * @param {string} teamName
26
+ * @param {object} opts - { fix_max?, ralph_max? }
27
+ * @returns {object} 파이프라인 API
28
+ */
29
+ export function createPipeline(db, teamName, opts = {}) {
30
+ ensurePipelineTable(db);
31
+
32
+ // 기존 상태가 있으면 로드, 없으면 초기화
33
+ let state = readPipelineState(db, teamName);
34
+ if (!state) {
35
+ state = initPipelineState(db, teamName, opts);
36
+ }
37
+
38
+ return {
39
+ /**
40
+ * 현재 상태 조회
41
+ */
42
+ getState() {
43
+ state = readPipelineState(db, teamName) || state;
44
+ return { ...state };
45
+ },
46
+
47
+ /**
48
+ * 다음 단계로 전이 가능 여부
49
+ * @param {string} phase
50
+ */
51
+ canAdvance(phase) {
52
+ const current = readPipelineState(db, teamName);
53
+ return current ? canTransition(current.phase, phase) : false;
54
+ },
55
+
56
+ /**
57
+ * 다음 단계로 전이
58
+ * @param {string} nextPhase
59
+ * @returns {{ ok: boolean, state?: object, error?: string }}
60
+ */
61
+ advance(nextPhase) {
62
+ const current = readPipelineState(db, teamName);
63
+ if (!current) {
64
+ return { ok: false, error: `파이프라인 없음: ${teamName}` };
65
+ }
66
+
67
+ const result = transitionPhase(current, nextPhase);
68
+ if (!result.ok) return result;
69
+
70
+ state = updatePipelineState(db, teamName, result.state);
71
+ return { ok: true, state: { ...state } };
72
+ },
73
+
74
+ /**
75
+ * ralph loop 재시작 (plan부터 다시)
76
+ * @returns {{ ok: boolean, state?: object, error?: string }}
77
+ */
78
+ restart() {
79
+ const current = readPipelineState(db, teamName);
80
+ if (!current) {
81
+ return { ok: false, error: `파이프라인 없음: ${teamName}` };
82
+ }
83
+
84
+ const result = ralphRestart(current);
85
+ if (!result.ok) return result;
86
+
87
+ state = updatePipelineState(db, teamName, result.state);
88
+ return { ok: true, state: { ...state } };
89
+ },
90
+
91
+ /**
92
+ * DAG 컨텍스트를 파이프라인 상태에 저장
93
+ * @param {{ dag_width: number, levels: Record<number, string[]>, edges: Array<{from:string, to:string}>, max_complexity: string, taskResults: Record<string, *> }} dagContext
94
+ */
95
+ setDagContext(dagContext) {
96
+ const current = readPipelineState(db, teamName);
97
+ if (!current) return;
98
+ const artifacts = { ...(current.artifacts || {}), dagContext };
99
+ state = updatePipelineState(db, teamName, { artifacts });
100
+ },
101
+
102
+ /**
103
+ * DAG 컨텍스트 조회 (편의 메서드)
104
+ * @returns {{ dag_width: number, levels: Record<number, string[]>, edges: Array<{from:string, to:string}>, max_complexity: string, taskResults: Record<string, *> } | null}
105
+ */
106
+ getDagContext() {
107
+ const current = readPipelineState(db, teamName) || state;
108
+ return current?.artifacts?.dagContext || null;
109
+ },
110
+
111
+ /**
112
+ * artifact 저장 (plan_path, prd_path, verify_report 등)
113
+ * @param {string} key
114
+ * @param {*} value
115
+ */
116
+ setArtifact(key, value) {
117
+ const current = readPipelineState(db, teamName);
118
+ if (!current) return;
119
+ const artifacts = { ...(current.artifacts || {}), [key]: value };
120
+ state = updatePipelineState(db, teamName, { artifacts });
121
+ },
122
+
123
+ /**
124
+ * Plan 파일을 .tfx/plans/{teamName}-plan.md 에 기록하고
125
+ * artifact('plan_path')에 절대 경로를 저장한다.
126
+ * @param {string} content - Plan markdown 내용
127
+ * @returns {string} 절대 경로
128
+ */
129
+ writePlanFile(content) {
130
+ const safeName = teamName.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
131
+ const planDir = resolve(process.cwd(), '.tfx', 'plans');
132
+ mkdirSync(planDir, { recursive: true });
133
+ const planPath = join(planDir, `${safeName}-plan.md`);
134
+ writeFileSync(planPath, content, 'utf8');
135
+ this.setArtifact('plan_path', planPath);
136
+ return planPath;
137
+ },
138
+
139
+ /**
140
+ * 터미널 상태 여부
141
+ */
142
+ isTerminal() {
143
+ const current = readPipelineState(db, teamName);
144
+ return current ? TERMINAL.has(current.phase) : true;
145
+ },
146
+
147
+ /**
148
+ * 파이프라인 초기화 (리셋)
149
+ */
150
+ reset() {
151
+ state = initPipelineState(db, teamName, opts);
152
+ return { ...state };
153
+ },
154
+
155
+ /**
156
+ * 파이프라인 삭제
157
+ */
158
+ remove() {
159
+ return removePipelineState(db, teamName);
160
+ },
161
+
162
+ /**
163
+ * Confidence Gate 실행 + 자동 전이
164
+ * prd → confidence → exec/failed
165
+ * @param {string|object} planArtifact
166
+ * @param {object} context - { checks?, codebaseFiles?, existingTests? }
167
+ * @returns {{ ok: boolean, gate: object, state?: object, error?: string }}
168
+ */
169
+ runConfidenceGate(planArtifact, context = {}) {
170
+ const current = readPipelineState(db, teamName);
171
+ if (!current) return { ok: false, error: `파이프라인 없음: ${teamName}` };
172
+
173
+ if (current.phase !== 'confidence') {
174
+ return { ok: false, error: `confidence gate는 confidence 단계에서만 실행 가능 (현재: ${current.phase})` };
175
+ }
176
+
177
+ const gate = runConfidenceCheck(planArtifact, context);
178
+ this.setArtifact('confidence_result', gate);
179
+
180
+ if (gate.decision === 'abort') {
181
+ const result = this.advance('failed');
182
+ return { ok: true, gate, state: result.state };
183
+ }
184
+
185
+ // proceed 또는 alternative → exec로 전이
186
+ const result = this.advance('exec');
187
+ return { ok: result.ok, gate, state: result.state, error: result.error };
188
+ },
189
+
190
+ /**
191
+ * Deslop Gate 실행 + 자동 전이
192
+ * exec → deslop → verify
193
+ * 호출자가 미리 deslop 결과를 생성하여 전달.
194
+ * @param {object} [deslopResult] - scanDirectory() 또는 detectSlop() 결과
195
+ * @returns {{ ok: boolean, gate: object, state?: object, error?: string }}
196
+ */
197
+ runDeslopGate(deslopResult = null) {
198
+ const current = readPipelineState(db, teamName);
199
+ if (!current) return { ok: false, error: `파이프라인 없음: ${teamName}` };
200
+
201
+ if (current.phase !== 'deslop') {
202
+ return { ok: false, error: `deslop gate는 deslop 단계에서만 실행 가능 (현재: ${current.phase})` };
203
+ }
204
+
205
+ const gate = deslopResult || { files: [], summary: { total: 0, clean: 0 } };
206
+ this.setArtifact('deslop_result', gate);
207
+
208
+ // deslop은 항상 verify로 전이 (정보 제공 게이트, 차단 없음)
209
+ const result = this.advance('verify');
210
+ return { ok: result.ok, gate, state: result.state, error: result.error };
211
+ },
212
+
213
+ /**
214
+ * Self-Check Gate 실행 + 자동 전이
215
+ * verify → selfcheck → complete/fix
216
+ * @param {string|object} execResult
217
+ * @param {string|object} verifyResult
218
+ * @param {object} requirements - { hasDiff?, evidence? }
219
+ * @returns {{ ok: boolean, gate: object, state?: object, error?: string }}
220
+ */
221
+ runSelfCheckGate(execResult, verifyResult, requirements = {}) {
222
+ const current = readPipelineState(db, teamName);
223
+ if (!current) return { ok: false, error: `파이프라인 없음: ${teamName}` };
224
+
225
+ if (current.phase !== 'selfcheck') {
226
+ return { ok: false, error: `selfcheck gate는 selfcheck 단계에서만 실행 가능 (현재: ${current.phase})` };
227
+ }
228
+
229
+ const gate = runSelfCheck(execResult, verifyResult, requirements);
230
+ this.setArtifact('selfcheck_result', gate);
231
+
232
+ if (gate.passed) {
233
+ const result = this.advance('complete');
234
+ return { ok: result.ok, gate, state: result.state, error: result.error };
235
+ }
236
+
237
+ // Red Flag 탐지 또는 필수 질문 실패 → fix
238
+ const result = this.advance('fix');
239
+ return { ok: result.ok, gate, state: result.state, error: result.error };
240
+ },
241
+ };
242
+ }
243
+
244
+ // ── 토큰 벤치마크 훅 ──
245
+
246
+ let _tokenSnapshotMod = null;
247
+
248
+ async function loadTokenSnapshot() {
249
+ if (_tokenSnapshotMod) return _tokenSnapshotMod;
250
+ try {
251
+ _tokenSnapshotMod = await import('../../scripts/token-snapshot.mjs');
252
+ } catch {
253
+ _tokenSnapshotMod = null;
254
+ }
255
+ return _tokenSnapshotMod;
256
+ }
257
+
258
+ /**
259
+ * 파이프라인 시작 시 토큰 스냅샷 캡처
260
+ * @param {string} label - 스냅샷 라벨 (e.g. teamName + timestamp)
261
+ * @returns {Promise<{label: string, snapshot: object}|null>}
262
+ */
263
+ export async function benchmarkStart(label) {
264
+ const mod = await loadTokenSnapshot();
265
+ if (!mod?.takeSnapshot) return null;
266
+ try {
267
+ const snapshot = mod.takeSnapshot(label);
268
+ return { label, snapshot };
269
+ } catch { return null; }
270
+ }
271
+
272
+ /**
273
+ * 파이프라인 종료 시 diff 계산 + 결과 저장
274
+ * @param {string} preLabel - 시작 스냅샷 라벨
275
+ * @param {string} postLabel - 종료 스냅샷 라벨
276
+ * @param {object} options - { agent?, cli?, id? }
277
+ * @returns {Promise<object|null>} diff 결과
278
+ */
279
+ export async function benchmarkEnd(preLabel, postLabel, options = {}) {
280
+ const mod = await loadTokenSnapshot();
281
+ if (!mod?.takeSnapshot || !mod?.computeDiff) return null;
282
+ try {
283
+ // 종료 스냅샷 캡처
284
+ mod.takeSnapshot(postLabel);
285
+ // diff 계산 (결과는 DIFFS_DIR에 자동 저장됨)
286
+ const diff = mod.computeDiff(preLabel, postLabel, options);
287
+
288
+ // 추가로 타임스탬프 기반 사본 저장
289
+ const diffsDir = join(homedir(), '.omc', 'state', 'cx-auto-tokens', 'diffs');
290
+ mkdirSync(diffsDir, { recursive: true });
291
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
292
+ const outPath = join(diffsDir, `${ts}.json`);
293
+ writeFileSync(outPath, JSON.stringify(diff, null, 2));
294
+
295
+ return diff;
296
+ } catch { return null; }
297
+ }
298
+
299
+ /**
300
+ * 트리아지 통합: quickClassify 고신뢰 시 Codex 분류 스킵 판정
301
+ * @param {string} prompt
302
+ * @param {number} [threshold=0.8]
303
+ * @returns {{ skip: boolean, routing: object|null, classification: object }}
304
+ */
305
+ export function triageWithIntent(prompt, threshold = 0.8) {
306
+ const classification = _classifyIntent(prompt);
307
+ if (classification.confidence >= threshold) {
308
+ return { skip: true, routing: classification.routing, classification };
309
+ }
310
+ return { skip: false, routing: null, classification };
311
+ }
312
+
313
+ export { ensurePipelineTable } from './state.mjs';
314
+ export { PHASES, TERMINAL, ALLOWED, canTransition } from './transitions.mjs';
315
+ export { CRITERIA, runConfidenceCheck } from './gates/confidence.mjs';
316
+ export { RED_FLAGS, QUESTIONS, runSelfCheck } from './gates/selfcheck.mjs';
317
+ export { detectSlop, autoFixSlop, scanDirectory } from '../quality/deslop.mjs';
318
+ export { quickClassify, classifyIntent, INTENT_CATEGORIES } from '../intent.mjs';
@@ -0,0 +1,191 @@
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 { TFX_STATE_DIR, ensureTfxDirs } 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
+ }