triflux 7.1.4 → 7.2.2

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 (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +725 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1671 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. package/.mcp.json +0 -8
@@ -52,5 +52,5 @@ export function runConfidenceCheck(planArtifact, context = {}) {
52
52
  reasoning = `확신도 ${totalScore}%: 기준 미달. 실행 중단.`;
53
53
  }
54
54
 
55
- return { score: totalScore, breakdown, decision, reasoning };
55
+ return { score: totalScore, breakdown, decision, reasoning, needsReview: decision === 'alternative' };
56
56
  }
@@ -58,10 +58,8 @@ function detectRedFlags(text, context = {}) {
58
58
  * @returns {{ passed: boolean, score: number, flags: Array, checklist: Array }}
59
59
  */
60
60
  export function runSelfCheck(execResult, verifyResult, requirements = {}) {
61
- const text = [
62
- typeof execResult === 'string' ? execResult : JSON.stringify(execResult || ''),
63
- typeof verifyResult === 'string' ? verifyResult : JSON.stringify(verifyResult || ''),
64
- ].join('\n');
61
+ const normalize = (v) => typeof v === 'string' ? v : (v != null ? JSON.stringify(v) : '');
62
+ const text = [normalize(execResult), normalize(verifyResult)].join('\n');
65
63
 
66
64
  const flags = detectRedFlags(text, requirements);
67
65
 
@@ -1,187 +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
- const S = getStatements(db);
114
- const now = Date.now();
115
- const state = {
116
- team_name: teamName,
117
- phase: 'plan',
118
- fix_attempt: 0,
119
- fix_max: opts.fix_max ?? 3,
120
- ralph_iteration: 0,
121
- ralph_max: opts.ralph_max ?? 10,
122
- artifacts: {},
123
- phase_history: [],
124
- created_at: now,
125
- updated_at: now,
126
- };
127
-
128
- // 기존 상태가 있으면 삭제 후 재생성
129
- S.remove.run(teamName);
130
- S.insert.run(serializeState(state));
131
- return state;
132
- }
133
-
134
- /**
135
- * 파이프라인 상태 조회
136
- * @param {object} db - better-sqlite3 인스턴스
137
- * @param {string} teamName
138
- * @returns {object|null}
139
- */
140
- export function readPipelineState(db, teamName) {
141
- const S = getStatements(db);
142
- return parseRow(S.get.get(teamName));
143
- }
144
-
145
- /**
146
- * 파이프라인 상태 업데이트 (부분 패치)
147
- * @param {object} db - better-sqlite3 인스턴스
148
- * @param {string} teamName
149
- * @param {object} patch - 업데이트할 필드
150
- * @returns {object|null} 업데이트된 상태
151
- */
152
- export function updatePipelineState(db, teamName, patch) {
153
- const S = getStatements(db);
154
- const current = parseRow(S.get.get(teamName));
155
- if (!current) return null;
156
-
157
- const merged = {
158
- ...current,
159
- ...patch,
160
- team_name: teamName, // team_name 변경 불가
161
- updated_at: Date.now(),
162
- };
163
-
164
- S.update.run(serializeState(merged));
165
- return merged;
166
- }
167
-
168
- /**
169
- * 파이프라인 상태 삭제
170
- * @param {object} db - better-sqlite3 인스턴스
171
- * @param {string} teamName
172
- * @returns {boolean}
173
- */
174
- export function removePipelineState(db, teamName) {
175
- const S = getStatements(db);
176
- return S.remove.run(teamName).changes > 0;
177
- }
178
-
179
- /**
180
- * 활성 파이프라인 목록
181
- * @param {object} db - better-sqlite3 인스턴스
182
- * @returns {object[]}
183
- */
184
- export function listPipelineStates(db) {
185
- const S = getStatements(db);
186
- return S.list.all().map(parseRow);
187
- }
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
+ }
@@ -1,120 +1,124 @@
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', 'prd', 'confidence', 'exec', 'deslop', 'verify', 'selfcheck',
9
- 'fix', 'complete', 'failed',
10
- ];
11
-
12
- export const TERMINAL = new Set(['complete', 'failed']);
13
-
14
- export const ALLOWED = {
15
- 'plan': ['prd'],
16
- 'prd': ['confidence'],
17
- 'confidence': ['exec', 'failed'],
18
- 'exec': ['deslop'],
19
- 'deslop': ['verify'],
20
- 'verify': ['selfcheck', 'fix', 'failed'],
21
- 'selfcheck': ['complete', 'fix'],
22
- 'fix': ['exec', 'verify', 'complete', 'failed'],
23
- 'complete': [],
24
- 'failed': [],
25
- };
26
-
27
- /**
28
- * 전이 가능 여부 확인
29
- * @param {string} from - 현재 단계
30
- * @param {string} to - 다음 단계
31
- * @returns {boolean}
32
- */
33
- export function canTransition(from, to) {
34
- const targets = ALLOWED[from];
35
- if (!targets) return false;
36
- return targets.includes(to);
37
- }
38
-
39
- /**
40
- * 상태 전이 실행 — fix loop 바운딩 포함
41
- * @param {object} state - 파이프라인 상태 객체
42
- * @param {string} nextPhase - 다음 단계
43
- * @returns {{ ok: boolean, state?: object, error?: string }}
44
- */
45
- export function transitionPhase(state, nextPhase) {
46
- const current = state.phase;
47
-
48
- if (!canTransition(current, nextPhase)) {
49
- return {
50
- ok: false,
51
- error: `전이 불가: ${current} → ${nextPhase}. 허용: [${(ALLOWED[current] || []).join(', ')}]`,
52
- };
53
- }
54
-
55
- const next = { ...state, phase: nextPhase, updated_at: Date.now() };
56
-
57
- // fix 단계 진입 시 attempt 증가 + 바운딩
58
- if (nextPhase === 'fix') {
59
- next.fix_attempt = (state.fix_attempt || 0) + 1;
60
- if (next.fix_attempt > (state.fix_max || 3)) {
61
- return {
62
- ok: false,
63
- error: `fix loop 초과: ${next.fix_attempt}/${state.fix_max || 3}회. ralph loop로 승격 필요.`,
64
- };
65
- }
66
- }
67
-
68
- // fix → exec 재진입 시 (fix 후 재실행)
69
- if (current === 'fix' && nextPhase === 'exec') {
70
- // fix_attempt 유지 (이미 fix 진입 시 증가됨)
71
- }
72
-
73
- // verify → fix → ... → verify 반복 후 fix_max 초과 시 ralph loop
74
- if (nextPhase === 'failed' && current === 'fix') {
75
- // ralph loop 반복 증가
76
- next.ralph_iteration = (state.ralph_iteration || 0) + 1;
77
- if (next.ralph_iteration > (state.ralph_max || 10)) {
78
- // 최종 실패 — ralph loop도 초과
79
- next.phase = 'failed';
80
- }
81
- }
82
-
83
- // phase_history 기록
84
- const history = Array.isArray(state.phase_history) ? [...state.phase_history] : [];
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
- const iteration = (state.ralph_iteration || 0) + 1;
99
- if (iteration > (state.ralph_max || 10)) {
100
- return {
101
- ok: false,
102
- error: `ralph loop 초과: ${iteration}/${state.ralph_max || 10}회. 최종 실패.`,
103
- };
104
- }
105
-
106
- const history = Array.isArray(state.phase_history) ? [...state.phase_history] : [];
107
- history.push({ from: state.phase, to: 'plan', at: Date.now(), ralph_restart: true });
108
-
109
- return {
110
- ok: true,
111
- state: {
112
- ...state,
113
- phase: 'plan',
114
- fix_attempt: 0,
115
- ralph_iteration: iteration,
116
- phase_history: history,
117
- updated_at: Date.now(),
118
- },
119
- };
120
- }
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', 'prd', 'confidence', 'exec', 'deslop', 'verify', 'selfcheck',
9
+ 'fix', 'complete', 'failed',
10
+ ];
11
+
12
+ export const TERMINAL = new Set(['complete', 'failed']);
13
+
14
+ export const ALLOWED = {
15
+ 'plan': ['prd'],
16
+ 'prd': ['confidence'],
17
+ 'confidence': ['exec', 'failed'],
18
+ 'exec': ['deslop'],
19
+ 'deslop': ['verify'],
20
+ 'verify': ['selfcheck', 'fix', 'failed'],
21
+ 'selfcheck': ['complete', 'fix'],
22
+ 'fix': ['exec', 'verify', 'complete', 'failed'],
23
+ 'complete': [],
24
+ 'failed': [],
25
+ };
26
+
27
+ /**
28
+ * 전이 가능 여부 확인
29
+ * @param {string} from - 현재 단계
30
+ * @param {string} to - 다음 단계
31
+ * @returns {boolean}
32
+ */
33
+ export function canTransition(from, to) {
34
+ const targets = ALLOWED[from];
35
+ if (!targets) return false;
36
+ return targets.includes(to);
37
+ }
38
+
39
+ /**
40
+ * 상태 전이 실행 — fix loop 바운딩 포함
41
+ * @param {object} state - 파이프라인 상태 객체
42
+ * @param {string} nextPhase - 다음 단계
43
+ * @returns {{ ok: boolean, state?: object, error?: string }}
44
+ */
45
+ export function transitionPhase(state, nextPhase) {
46
+ const current = state.phase;
47
+
48
+ if (!canTransition(current, nextPhase)) {
49
+ return {
50
+ ok: false,
51
+ error: `전이 불가: ${current} → ${nextPhase}. 허용: [${(ALLOWED[current] || []).join(', ')}]`,
52
+ };
53
+ }
54
+
55
+ const next = { ...state, phase: nextPhase, updated_at: Date.now() };
56
+
57
+ // fix 단계 진입 시 attempt 증가 + 바운딩
58
+ if (nextPhase === 'fix') {
59
+ next.fix_attempt = (state.fix_attempt || 0) + 1;
60
+ if (next.fix_attempt > (state.fix_max || 3)) {
61
+ return {
62
+ ok: false,
63
+ error: `fix loop 초과: ${state.fix_max || 3} 도달`,
64
+ };
65
+ }
66
+ }
67
+
68
+ // fix → exec 재진입 시 (fix 후 재실행)
69
+ if (current === 'fix' && nextPhase === 'exec') {
70
+ // fix_attempt 유지 (이미 fix 진입 시 증가됨)
71
+ }
72
+
73
+ // verify → fix → ... → verify 반복 후 fix_max 초과 시 ralph loop
74
+ if (nextPhase === 'failed' && current === 'fix') {
75
+ // ralph loop 반복 증가
76
+ next.ralph_iteration = (state.ralph_iteration || 0) + 1;
77
+ if (next.ralph_iteration > (state.ralph_max || 10)) {
78
+ // 최종 실패 — ralph loop도 초과
79
+ next.phase = 'failed';
80
+ }
81
+ }
82
+
83
+ // phase_history 기록
84
+ const history = Array.isArray(state.phase_history) ? [...state.phase_history] : [];
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) ? [...state.phase_history] : [];
111
+ history.push({ from: state.phase, to: 'plan', at: Date.now(), ralph_restart: true });
112
+
113
+ return {
114
+ ok: true,
115
+ state: {
116
+ ...state,
117
+ phase: 'plan',
118
+ fix_attempt: 0,
119
+ ralph_iteration: iteration,
120
+ phase_history: history,
121
+ updated_at: Date.now(),
122
+ },
123
+ };
124
+ }