triflux 9.7.13 → 9.8.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +2 -0
- package/README.md +2 -0
- package/bin/triflux.mjs +297 -47
- package/hooks/hook-registry.json +4 -4
- package/hub/fullcycle.mjs +96 -0
- package/hub/paths.mjs +30 -28
- package/hub/pipeline/index.mjs +318 -318
- package/hub/schema.sql +146 -146
- package/hub/team/cli/commands/kill.mjs +37 -37
- package/hub/team/cli/commands/stop.mjs +31 -31
- package/hub/team/cli/commands/task.mjs +30 -30
- package/hub/team/cli/services/hub-client.mjs +208 -208
- package/hub/team/cli/services/native-control.mjs +118 -118
- package/hub/team/cli/services/runtime-mode.mjs +62 -62
- package/hub/team/cli/services/state-store.mjs +48 -48
- package/hub/team/dashboard.mjs +274 -274
- package/hub/team/native.mjs +649 -649
- package/hub/team/psmux.mjs +68 -13
- package/hub/tools.mjs +554 -554
- package/hub/workers/claude-worker.mjs +423 -423
- package/hub/workers/codex-mcp.mjs +410 -410
- package/hub/workers/gemini-worker.mjs +429 -429
- package/hub/workers/interface.mjs +40 -40
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +1 -1
- package/scripts/cache-warmup.mjs +1 -0
- package/scripts/claude-logged.ps1 +54 -0
- package/scripts/demo-tui.mjs +59 -0
- package/scripts/headless-guard.mjs +4 -7
- package/scripts/hub-ensure.mjs +120 -120
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +1 -1
- package/scripts/setup.mjs +150 -6
- package/scripts/tfx-route-post.mjs +90 -13
- package/scripts/token-snapshot.mjs +575 -575
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/tfx-codex-swarm/SKILL.md +40 -5
- package/skills/tfx-codex-swarm/mcp-daemon/register-autostart.ps1 +32 -0
- package/skills/tfx-doctor/SKILL.md +3 -0
- package/skills/tfx-fullcycle/SKILL.md +79 -4
- package/skills/tfx-hub/SKILL.md +3 -1
- package/skills/tfx-psmux-rules/SKILL.md +53 -31
- package/skills/tfx-remote-spawn/references/hosts.json +16 -16
- package/skills/tfx-setup/SKILL.md +9 -0
- package/tui/doctor.mjs +1 -0
package/hub/pipeline/index.mjs
CHANGED
|
@@ -1,318 +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';
|
|
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';
|