@litmers/cursorflow-orchestrator 0.1.40 → 0.2.3

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 (214) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/README.md +8 -3
  3. package/commands/cursorflow-init.md +0 -4
  4. package/dist/cli/index.js +0 -6
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/logs.js +108 -9
  7. package/dist/cli/logs.js.map +1 -1
  8. package/dist/cli/models.js +20 -3
  9. package/dist/cli/models.js.map +1 -1
  10. package/dist/cli/monitor.d.ts +7 -10
  11. package/dist/cli/monitor.js +1103 -1239
  12. package/dist/cli/monitor.js.map +1 -1
  13. package/dist/cli/resume.js +21 -1
  14. package/dist/cli/resume.js.map +1 -1
  15. package/dist/cli/run.js +28 -9
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/cli/signal.d.ts +6 -1
  18. package/dist/cli/signal.js +99 -13
  19. package/dist/cli/signal.js.map +1 -1
  20. package/dist/cli/tasks.js +3 -46
  21. package/dist/cli/tasks.js.map +1 -1
  22. package/dist/core/agent-supervisor.d.ts +23 -0
  23. package/dist/core/agent-supervisor.js +42 -0
  24. package/dist/core/agent-supervisor.js.map +1 -0
  25. package/dist/core/auto-recovery.d.ts +3 -117
  26. package/dist/core/auto-recovery.js +4 -482
  27. package/dist/core/auto-recovery.js.map +1 -1
  28. package/dist/core/failure-policy.d.ts +0 -53
  29. package/dist/core/failure-policy.js +7 -175
  30. package/dist/core/failure-policy.js.map +1 -1
  31. package/dist/core/git-lifecycle-manager.d.ts +284 -0
  32. package/dist/core/git-lifecycle-manager.js +778 -0
  33. package/dist/core/git-lifecycle-manager.js.map +1 -0
  34. package/dist/core/git-pipeline-coordinator.d.ts +21 -0
  35. package/dist/core/git-pipeline-coordinator.js +205 -0
  36. package/dist/core/git-pipeline-coordinator.js.map +1 -0
  37. package/dist/core/intervention.d.ts +170 -0
  38. package/dist/core/intervention.js +408 -0
  39. package/dist/core/intervention.js.map +1 -0
  40. package/dist/core/lane-state-machine.d.ts +423 -0
  41. package/dist/core/lane-state-machine.js +890 -0
  42. package/dist/core/lane-state-machine.js.map +1 -0
  43. package/dist/core/orchestrator.d.ts +4 -1
  44. package/dist/core/orchestrator.js +39 -65
  45. package/dist/core/orchestrator.js.map +1 -1
  46. package/dist/core/runner/agent.d.ts +7 -1
  47. package/dist/core/runner/agent.js +54 -36
  48. package/dist/core/runner/agent.js.map +1 -1
  49. package/dist/core/runner/pipeline.js +283 -123
  50. package/dist/core/runner/pipeline.js.map +1 -1
  51. package/dist/core/runner/task.d.ts +4 -5
  52. package/dist/core/runner/task.js +6 -80
  53. package/dist/core/runner/task.js.map +1 -1
  54. package/dist/core/runner.js +8 -2
  55. package/dist/core/runner.js.map +1 -1
  56. package/dist/core/stall-detection.d.ts +11 -4
  57. package/dist/core/stall-detection.js +64 -27
  58. package/dist/core/stall-detection.js.map +1 -1
  59. package/dist/hooks/contexts/index.d.ts +104 -0
  60. package/dist/hooks/contexts/index.js +134 -0
  61. package/dist/hooks/contexts/index.js.map +1 -0
  62. package/dist/hooks/data-accessor.d.ts +86 -0
  63. package/dist/hooks/data-accessor.js +410 -0
  64. package/dist/hooks/data-accessor.js.map +1 -0
  65. package/dist/hooks/flow-controller.d.ts +136 -0
  66. package/dist/hooks/flow-controller.js +351 -0
  67. package/dist/hooks/flow-controller.js.map +1 -0
  68. package/dist/hooks/index.d.ts +68 -0
  69. package/dist/hooks/index.js +105 -0
  70. package/dist/hooks/index.js.map +1 -0
  71. package/dist/hooks/manager.d.ts +129 -0
  72. package/dist/hooks/manager.js +389 -0
  73. package/dist/hooks/manager.js.map +1 -0
  74. package/dist/hooks/types.d.ts +463 -0
  75. package/dist/hooks/types.js +45 -0
  76. package/dist/hooks/types.js.map +1 -0
  77. package/dist/services/logging/buffer.d.ts +2 -2
  78. package/dist/services/logging/buffer.js +95 -42
  79. package/dist/services/logging/buffer.js.map +1 -1
  80. package/dist/services/logging/console.js +6 -1
  81. package/dist/services/logging/console.js.map +1 -1
  82. package/dist/services/logging/formatter.d.ts +9 -4
  83. package/dist/services/logging/formatter.js +64 -18
  84. package/dist/services/logging/formatter.js.map +1 -1
  85. package/dist/services/logging/index.d.ts +0 -1
  86. package/dist/services/logging/index.js +0 -1
  87. package/dist/services/logging/index.js.map +1 -1
  88. package/dist/services/logging/paths.d.ts +8 -0
  89. package/dist/services/logging/paths.js +48 -0
  90. package/dist/services/logging/paths.js.map +1 -0
  91. package/dist/services/logging/raw-log.d.ts +6 -0
  92. package/dist/services/logging/raw-log.js +37 -0
  93. package/dist/services/logging/raw-log.js.map +1 -0
  94. package/dist/services/process/index.js +1 -1
  95. package/dist/services/process/index.js.map +1 -1
  96. package/dist/types/agent.d.ts +15 -0
  97. package/dist/types/config.d.ts +22 -1
  98. package/dist/types/event-categories.d.ts +601 -0
  99. package/dist/types/event-categories.js +233 -0
  100. package/dist/types/event-categories.js.map +1 -0
  101. package/dist/types/events.d.ts +0 -20
  102. package/dist/types/flow.d.ts +10 -6
  103. package/dist/types/index.d.ts +1 -1
  104. package/dist/types/index.js +17 -3
  105. package/dist/types/index.js.map +1 -1
  106. package/dist/types/lane.d.ts +1 -1
  107. package/dist/types/logging.d.ts +1 -1
  108. package/dist/types/task.d.ts +12 -1
  109. package/dist/ui/log-viewer.d.ts +3 -0
  110. package/dist/ui/log-viewer.js +3 -0
  111. package/dist/ui/log-viewer.js.map +1 -1
  112. package/dist/utils/config.js +10 -1
  113. package/dist/utils/config.js.map +1 -1
  114. package/dist/utils/cursor-agent.d.ts +11 -1
  115. package/dist/utils/cursor-agent.js +63 -16
  116. package/dist/utils/cursor-agent.js.map +1 -1
  117. package/dist/utils/enhanced-logger.d.ts +5 -1
  118. package/dist/utils/enhanced-logger.js +98 -19
  119. package/dist/utils/enhanced-logger.js.map +1 -1
  120. package/dist/utils/event-registry.d.ts +222 -0
  121. package/dist/utils/event-registry.js +463 -0
  122. package/dist/utils/event-registry.js.map +1 -0
  123. package/dist/utils/events.d.ts +1 -13
  124. package/dist/utils/events.js.map +1 -1
  125. package/dist/utils/flow.d.ts +10 -0
  126. package/dist/utils/flow.js +75 -0
  127. package/dist/utils/flow.js.map +1 -1
  128. package/dist/utils/log-constants.d.ts +1 -0
  129. package/dist/utils/log-constants.js +2 -1
  130. package/dist/utils/log-constants.js.map +1 -1
  131. package/dist/utils/log-formatter.d.ts +2 -1
  132. package/dist/utils/log-formatter.js +10 -10
  133. package/dist/utils/log-formatter.js.map +1 -1
  134. package/dist/utils/logger.d.ts +11 -0
  135. package/dist/utils/logger.js +82 -3
  136. package/dist/utils/logger.js.map +1 -1
  137. package/dist/utils/repro-thinking-logs.js +0 -13
  138. package/dist/utils/repro-thinking-logs.js.map +1 -1
  139. package/dist/utils/run-service.js +1 -1
  140. package/dist/utils/run-service.js.map +1 -1
  141. package/examples/README.md +0 -2
  142. package/examples/demo-project/README.md +1 -2
  143. package/package.json +13 -34
  144. package/scripts/setup-security.sh +0 -1
  145. package/scripts/test-log-parser.ts +171 -0
  146. package/scripts/verify-change.sh +272 -0
  147. package/src/cli/index.ts +0 -6
  148. package/src/cli/logs.ts +121 -10
  149. package/src/cli/models.ts +20 -3
  150. package/src/cli/monitor.ts +1273 -1342
  151. package/src/cli/resume.ts +27 -1
  152. package/src/cli/run.ts +29 -11
  153. package/src/cli/signal.ts +120 -18
  154. package/src/cli/tasks.ts +2 -59
  155. package/src/core/agent-supervisor.ts +64 -0
  156. package/src/core/auto-recovery.ts +14 -590
  157. package/src/core/failure-policy.ts +7 -229
  158. package/src/core/git-lifecycle-manager.ts +1011 -0
  159. package/src/core/git-pipeline-coordinator.ts +221 -0
  160. package/src/core/intervention.ts +463 -0
  161. package/src/core/lane-state-machine.ts +1097 -0
  162. package/src/core/orchestrator.ts +48 -64
  163. package/src/core/runner/agent.ts +77 -39
  164. package/src/core/runner/pipeline.ts +318 -138
  165. package/src/core/runner/task.ts +12 -97
  166. package/src/core/runner.ts +8 -2
  167. package/src/core/stall-detection.ts +74 -27
  168. package/src/hooks/contexts/index.ts +256 -0
  169. package/src/hooks/data-accessor.ts +488 -0
  170. package/src/hooks/flow-controller.ts +425 -0
  171. package/src/hooks/index.ts +154 -0
  172. package/src/hooks/manager.ts +434 -0
  173. package/src/hooks/types.ts +544 -0
  174. package/src/services/logging/buffer.ts +104 -43
  175. package/src/services/logging/console.ts +7 -1
  176. package/src/services/logging/formatter.ts +74 -18
  177. package/src/services/logging/index.ts +0 -2
  178. package/src/services/logging/paths.ts +14 -0
  179. package/src/services/logging/raw-log.ts +43 -0
  180. package/src/services/process/index.ts +1 -1
  181. package/src/types/agent.ts +15 -0
  182. package/src/types/config.ts +23 -1
  183. package/src/types/event-categories.ts +663 -0
  184. package/src/types/events.ts +0 -25
  185. package/src/types/flow.ts +10 -6
  186. package/src/types/index.ts +50 -4
  187. package/src/types/lane.ts +1 -2
  188. package/src/types/logging.ts +2 -1
  189. package/src/types/task.ts +12 -1
  190. package/src/ui/log-viewer.ts +3 -0
  191. package/src/utils/config.ts +11 -1
  192. package/src/utils/cursor-agent.ts +68 -16
  193. package/src/utils/enhanced-logger.ts +105 -19
  194. package/src/utils/event-registry.ts +595 -0
  195. package/src/utils/events.ts +0 -16
  196. package/src/utils/flow.ts +83 -0
  197. package/src/utils/log-constants.ts +2 -1
  198. package/src/utils/log-formatter.ts +10 -11
  199. package/src/utils/logger.ts +49 -3
  200. package/src/utils/repro-thinking-logs.ts +0 -15
  201. package/src/utils/run-service.ts +1 -1
  202. package/dist/cli/prepare.d.ts +0 -7
  203. package/dist/cli/prepare.js +0 -690
  204. package/dist/cli/prepare.js.map +0 -1
  205. package/dist/services/logging/file-writer.d.ts +0 -71
  206. package/dist/services/logging/file-writer.js +0 -516
  207. package/dist/services/logging/file-writer.js.map +0 -1
  208. package/dist/types/review.d.ts +0 -17
  209. package/dist/types/review.js +0 -6
  210. package/dist/types/review.js.map +0 -1
  211. package/scripts/ai-security-check.js +0 -233
  212. package/src/cli/prepare.ts +0 -777
  213. package/src/services/logging/file-writer.ts +0 -526
  214. package/src/types/review.ts +0 -20
@@ -0,0 +1,1097 @@
1
+ /**
2
+ * Lane State Machine
3
+ *
4
+ * Lane의 생명주기를 세분화된 상태로 관리하는 상태 머신입니다.
5
+ *
6
+ * 핵심 원칙:
7
+ * 1. 명시적 상태 전이 (Explicit State Transitions)
8
+ * 2. 단일 상태 저장소 (Single Source of Truth)
9
+ * 3. 상태 전이 검증 (Transition Validation)
10
+ * 4. 복구/재시도 로직 중앙화 (Centralized Recovery)
11
+ *
12
+ * 상태 계층:
13
+ * - 주 상태 (Primary State): Lane의 기본 상태 (pending, running, completed 등)
14
+ * - 부 상태 (Sub State): 세부 작업 상태 (preparing_git, executing_task 등)
15
+ * - 복구 상태 (Recovery State): 복구 진행 상태 (idle, monitoring, recovering 등)
16
+ */
17
+
18
+ import * as fs from 'fs';
19
+ import { events } from '../utils/events';
20
+ import { safeJoin } from '../utils/path';
21
+ import * as logger from '../utils/logger';
22
+ import {
23
+ StateEventType,
24
+ StateTransitionPayload,
25
+ StateTransitionFailedPayload,
26
+ } from '../types/event-categories';
27
+
28
+ // ============================================================================
29
+ // State Definitions (상태 정의)
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Lane 주 상태 (Primary State)
34
+ *
35
+ * 상태 전이 규칙:
36
+ * - PENDING → INITIALIZING → RUNNING → COMPLETED | FAILED
37
+ * - RUNNING ↔ WAITING (의존성 대기)
38
+ * - RUNNING ↔ PAUSED (사용자 요청)
39
+ * - RUNNING → RECOVERING → RUNNING | FAILED
40
+ * - * → ABORTED (강제 중단)
41
+ */
42
+ export enum LanePrimaryState {
43
+ /** 대기 중 - 아직 시작되지 않음 */
44
+ PENDING = 'pending',
45
+ /** 초기화 중 - 워크트리/브랜치 준비 */
46
+ INITIALIZING = 'initializing',
47
+ /** 실행 중 */
48
+ RUNNING = 'running',
49
+ /** 의존성 대기 중 */
50
+ WAITING = 'waiting',
51
+ /** 일시 중지 */
52
+ PAUSED = 'paused',
53
+ /** 복구 중 */
54
+ RECOVERING = 'recovering',
55
+ /** 완료 */
56
+ COMPLETED = 'completed',
57
+ /** 실패 */
58
+ FAILED = 'failed',
59
+ /** 강제 중단 */
60
+ ABORTED = 'aborted',
61
+ }
62
+
63
+ /**
64
+ * Lane 부 상태 (Sub State) - RUNNING 상태의 세부 단계
65
+ */
66
+ export enum LaneSubState {
67
+ /** 없음 (비실행 상태) */
68
+ NONE = 'none',
69
+
70
+ // --- Git 관련 ---
71
+ /** Git 워크트리 준비 */
72
+ PREPARING_GIT = 'preparing_git',
73
+ /** 의존성 브랜치 머지 */
74
+ MERGING_DEPENDENCIES = 'merging_dependencies',
75
+ /** Task 브랜치 생성 */
76
+ CREATING_TASK_BRANCH = 'creating_task_branch',
77
+ /** 변경사항 커밋 */
78
+ COMMITTING = 'committing',
79
+ /** 푸시 */
80
+ PUSHING = 'pushing',
81
+ /** 머지 */
82
+ MERGING = 'merging',
83
+
84
+ // --- Task 관련 ---
85
+ /** Task 실행 준비 */
86
+ PREPARING_TASK = 'preparing_task',
87
+ /** AI 에이전트와 통신 중 */
88
+ EXECUTING_AGENT = 'executing_agent',
89
+ /** Task 실행 완료 처리 */
90
+ FINALIZING_TASK = 'finalizing_task',
91
+
92
+ // --- 대기 관련 ---
93
+ /** 의존성 완료 대기 */
94
+ WAITING_DEPENDENCY = 'waiting_dependency',
95
+ /** 사용자 입력 대기 */
96
+ WAITING_USER_INPUT = 'waiting_user_input',
97
+ /** Rate limit 대기 */
98
+ WAITING_RATE_LIMIT = 'waiting_rate_limit',
99
+ }
100
+
101
+ /**
102
+ * 복구 상태 (Recovery State)
103
+ */
104
+ export enum RecoveryState {
105
+ /** 복구 필요 없음 */
106
+ IDLE = 'idle',
107
+ /** 모니터링 중 (Stall 감지) */
108
+ MONITORING = 'monitoring',
109
+ /** Continue 신호 발송됨 */
110
+ CONTINUE_SENT = 'continue_sent',
111
+ /** Stronger prompt 발송됨 */
112
+ STRONGER_PROMPT_SENT = 'stronger_prompt_sent',
113
+ /** 재시작 요청됨 */
114
+ RESTART_REQUESTED = 'restart_requested',
115
+ /** 진단 실행됨 */
116
+ DIAGNOSED = 'diagnosed',
117
+ /** 복구 포기 */
118
+ EXHAUSTED = 'exhausted',
119
+ }
120
+
121
+ // ============================================================================
122
+ // State Transition Triggers (상태 전이 트리거)
123
+ // ============================================================================
124
+
125
+ /**
126
+ * 상태 전이를 발생시키는 트리거
127
+ */
128
+ export enum StateTransitionTrigger {
129
+ // --- Lifecycle Triggers ---
130
+ /** Lane 시작 */
131
+ START = 'start',
132
+ /** 초기화 완료 */
133
+ INITIALIZED = 'initialized',
134
+ /** Task 완료 */
135
+ TASK_COMPLETED = 'task_completed',
136
+ /** 모든 Task 완료 */
137
+ ALL_TASKS_COMPLETED = 'all_tasks_completed',
138
+ /** 실패 발생 */
139
+ FAILURE = 'failure',
140
+ /** 강제 중단 요청 */
141
+ ABORT = 'abort',
142
+
143
+ // --- Dependency Triggers ---
144
+ /** 의존성 대기 시작 */
145
+ WAIT_DEPENDENCY = 'wait_dependency',
146
+ /** 의존성 해결됨 */
147
+ DEPENDENCY_RESOLVED = 'dependency_resolved',
148
+ /** 의존성 타임아웃 */
149
+ DEPENDENCY_TIMEOUT = 'dependency_timeout',
150
+ /** 의존성 실패 */
151
+ DEPENDENCY_FAILED = 'dependency_failed',
152
+
153
+ // --- Pause/Resume Triggers ---
154
+ /** 일시 중지 요청 */
155
+ PAUSE = 'pause',
156
+ /** 재개 요청 */
157
+ RESUME = 'resume',
158
+
159
+ // --- Recovery Triggers ---
160
+ /** 복구 시작 */
161
+ RECOVERY_START = 'recovery_start',
162
+ /** 복구 성공 */
163
+ RECOVERY_SUCCESS = 'recovery_success',
164
+ /** 복구 실패 */
165
+ RECOVERY_FAILED = 'recovery_failed',
166
+ /** Stall 감지 */
167
+ STALL_DETECTED = 'stall_detected',
168
+ /** Continue 신호 발송 */
169
+ CONTINUE_SIGNAL_SENT = 'continue_signal_sent',
170
+ /** Stronger prompt 발송 */
171
+ STRONGER_PROMPT_SENT = 'stronger_prompt_sent',
172
+ /** 재시작 요청 */
173
+ RESTART_REQUESTED = 'restart_requested',
174
+ /** 진단 완료 */
175
+ DIAGNOSIS_COMPLETE = 'diagnosis_complete',
176
+
177
+ // --- Sub-state Triggers ---
178
+ /** Git 작업 시작 */
179
+ GIT_START = 'git_start',
180
+ /** Git 작업 완료 */
181
+ GIT_COMPLETE = 'git_complete',
182
+ /** Task 실행 시작 */
183
+ TASK_START = 'task_start',
184
+ /** Agent 통신 시작 */
185
+ AGENT_START = 'agent_start',
186
+ /** Agent 응답 수신 */
187
+ AGENT_RESPONSE = 'agent_response',
188
+ }
189
+
190
+ // ============================================================================
191
+ // State Machine Configuration
192
+ // ============================================================================
193
+
194
+ /**
195
+ * 상태 전이 규칙 정의
196
+ */
197
+ interface TransitionRule {
198
+ from: LanePrimaryState | LanePrimaryState[];
199
+ to: LanePrimaryState;
200
+ trigger: StateTransitionTrigger;
201
+ /** 전이 전 검증 함수 */
202
+ guard?: (context: LaneStateContext) => boolean;
203
+ /** 전이 후 실행 함수 */
204
+ onTransition?: (context: LaneStateContext) => void;
205
+ }
206
+
207
+ /**
208
+ * Lane 상태 컨텍스트 (전체 상태 정보)
209
+ */
210
+ export interface LaneStateContext {
211
+ /** Lane 이름 */
212
+ laneName: string;
213
+ /** Run ID */
214
+ runId: string;
215
+ /** 주 상태 */
216
+ primaryState: LanePrimaryState;
217
+ /** 부 상태 */
218
+ subState: LaneSubState;
219
+ /** 복구 상태 */
220
+ recoveryState: RecoveryState;
221
+ /** 현재 Task 인덱스 */
222
+ currentTaskIndex: number;
223
+ /** 전체 Task 수 */
224
+ totalTasks: number;
225
+ /** 완료된 Task 목록 */
226
+ completedTasks: string[];
227
+ /** 현재 Task 이름 */
228
+ currentTaskName?: string;
229
+ /** 대기 중인 의존성 */
230
+ waitingFor?: string[];
231
+ /** 마지막 오류 */
232
+ lastError?: string;
233
+ /** 재시작 횟수 */
234
+ restartCount: number;
235
+ /** 시작 시간 */
236
+ startTime: number;
237
+ /** 종료 시간 */
238
+ endTime?: number;
239
+ /** 마지막 상태 변경 시간 */
240
+ lastTransitionTime: number;
241
+ /** 워크트리 디렉토리 */
242
+ worktreeDir?: string;
243
+ /** 파이프라인 브랜치 */
244
+ pipelineBranch?: string;
245
+ /** 현재 Task 브랜치 */
246
+ taskBranch?: string;
247
+ /** 프로세스 ID */
248
+ pid?: number;
249
+ /** 채팅 세션 ID */
250
+ chatId?: string;
251
+ /** 추가 메타데이터 */
252
+ metadata?: Record<string, any>;
253
+ }
254
+
255
+ /**
256
+ * 상태 전이 결과
257
+ */
258
+ export interface StateTransitionResult {
259
+ /** 성공 여부 */
260
+ success: boolean;
261
+ /** 이전 상태 */
262
+ fromState: LanePrimaryState;
263
+ /** 새 상태 */
264
+ toState: LanePrimaryState;
265
+ /** 트리거 */
266
+ trigger: StateTransitionTrigger;
267
+ /** 오류 메시지 (실패 시) */
268
+ error?: string;
269
+ }
270
+
271
+ /**
272
+ * 상태 전이 규칙 테이블
273
+ */
274
+ const TRANSITION_RULES: TransitionRule[] = [
275
+ // --- 시작 및 초기화 ---
276
+ { from: LanePrimaryState.PENDING, to: LanePrimaryState.INITIALIZING, trigger: StateTransitionTrigger.START },
277
+ { from: LanePrimaryState.INITIALIZING, to: LanePrimaryState.RUNNING, trigger: StateTransitionTrigger.INITIALIZED },
278
+ { from: LanePrimaryState.INITIALIZING, to: LanePrimaryState.FAILED, trigger: StateTransitionTrigger.FAILURE },
279
+
280
+ // --- 실행 중 → 완료/실패 ---
281
+ { from: LanePrimaryState.RUNNING, to: LanePrimaryState.COMPLETED, trigger: StateTransitionTrigger.ALL_TASKS_COMPLETED },
282
+ { from: LanePrimaryState.RUNNING, to: LanePrimaryState.FAILED, trigger: StateTransitionTrigger.FAILURE },
283
+
284
+ // --- 의존성 대기 ---
285
+ { from: LanePrimaryState.RUNNING, to: LanePrimaryState.WAITING, trigger: StateTransitionTrigger.WAIT_DEPENDENCY },
286
+ { from: LanePrimaryState.WAITING, to: LanePrimaryState.RUNNING, trigger: StateTransitionTrigger.DEPENDENCY_RESOLVED },
287
+ { from: LanePrimaryState.WAITING, to: LanePrimaryState.FAILED, trigger: StateTransitionTrigger.DEPENDENCY_TIMEOUT },
288
+ { from: LanePrimaryState.WAITING, to: LanePrimaryState.FAILED, trigger: StateTransitionTrigger.DEPENDENCY_FAILED },
289
+
290
+ // --- 일시 중지/재개 ---
291
+ { from: LanePrimaryState.RUNNING, to: LanePrimaryState.PAUSED, trigger: StateTransitionTrigger.PAUSE },
292
+ { from: LanePrimaryState.WAITING, to: LanePrimaryState.PAUSED, trigger: StateTransitionTrigger.PAUSE },
293
+ { from: LanePrimaryState.PAUSED, to: LanePrimaryState.RUNNING, trigger: StateTransitionTrigger.RESUME },
294
+
295
+ // --- 복구 ---
296
+ { from: LanePrimaryState.RUNNING, to: LanePrimaryState.RECOVERING, trigger: StateTransitionTrigger.RECOVERY_START },
297
+ { from: LanePrimaryState.RECOVERING, to: LanePrimaryState.RUNNING, trigger: StateTransitionTrigger.RECOVERY_SUCCESS },
298
+ { from: LanePrimaryState.RECOVERING, to: LanePrimaryState.FAILED, trigger: StateTransitionTrigger.RECOVERY_FAILED },
299
+
300
+ // --- 강제 중단 (모든 상태에서 가능) ---
301
+ {
302
+ from: [
303
+ LanePrimaryState.PENDING,
304
+ LanePrimaryState.INITIALIZING,
305
+ LanePrimaryState.RUNNING,
306
+ LanePrimaryState.WAITING,
307
+ LanePrimaryState.PAUSED,
308
+ LanePrimaryState.RECOVERING,
309
+ ],
310
+ to: LanePrimaryState.ABORTED,
311
+ trigger: StateTransitionTrigger.ABORT
312
+ },
313
+ ];
314
+
315
+ // ============================================================================
316
+ // Lane State Machine
317
+ // ============================================================================
318
+
319
+ /**
320
+ * Lane 상태 머신
321
+ *
322
+ * 사용 예:
323
+ * ```typescript
324
+ * const sm = LaneStateMachine.getInstance();
325
+ *
326
+ * // Lane 등록
327
+ * sm.registerLane('lane-1', 'run-123', {
328
+ * totalTasks: 5,
329
+ * pipelineBranch: 'cursorflow/pipeline/lane-1'
330
+ * });
331
+ *
332
+ * // 상태 전이
333
+ * sm.transition('lane-1', StateTransitionTrigger.START);
334
+ * sm.transition('lane-1', StateTransitionTrigger.INITIALIZED);
335
+ *
336
+ * // 부 상태 업데이트
337
+ * sm.setSubState('lane-1', LaneSubState.EXECUTING_AGENT);
338
+ *
339
+ * // 상태 조회
340
+ * const ctx = sm.getContext('lane-1');
341
+ * console.log(ctx.primaryState, ctx.subState);
342
+ * ```
343
+ */
344
+ export class LaneStateMachine {
345
+ private static instance: LaneStateMachine | null = null;
346
+
347
+ /** Lane별 상태 컨텍스트 */
348
+ private contexts: Map<string, LaneStateContext> = new Map();
349
+
350
+ /** 상태 히스토리 (디버깅/감사용) */
351
+ private history: Map<string, Array<{
352
+ timestamp: number;
353
+ fromState: LanePrimaryState;
354
+ toState: LanePrimaryState;
355
+ trigger: StateTransitionTrigger;
356
+ }>> = new Map();
357
+
358
+ /** 상태 변경 콜백 */
359
+ private onTransitionCallbacks: Array<(laneName: string, result: StateTransitionResult) => void> = [];
360
+
361
+ /** 디버그 모드 */
362
+ private verbose: boolean = false;
363
+
364
+ private constructor() {
365
+ this.verbose = process.env['DEBUG_STATE'] === 'true';
366
+ }
367
+
368
+ /**
369
+ * 싱글톤 인스턴스 획득
370
+ */
371
+ static getInstance(): LaneStateMachine {
372
+ if (!LaneStateMachine.instance) {
373
+ LaneStateMachine.instance = new LaneStateMachine();
374
+ }
375
+ return LaneStateMachine.instance;
376
+ }
377
+
378
+ /**
379
+ * 인스턴스 리셋 (테스트용)
380
+ */
381
+ static resetInstance(): void {
382
+ LaneStateMachine.instance = null;
383
+ }
384
+
385
+ // --------------------------------------------------------------------------
386
+ // Lane Registration
387
+ // --------------------------------------------------------------------------
388
+
389
+ /**
390
+ * Lane 등록
391
+ */
392
+ registerLane(
393
+ laneName: string,
394
+ runId: string,
395
+ options: {
396
+ totalTasks?: number;
397
+ pipelineBranch?: string;
398
+ worktreeDir?: string;
399
+ metadata?: Record<string, any>;
400
+ } = {}
401
+ ): LaneStateContext {
402
+ const now = Date.now();
403
+
404
+ const context: LaneStateContext = {
405
+ laneName,
406
+ runId,
407
+ primaryState: LanePrimaryState.PENDING,
408
+ subState: LaneSubState.NONE,
409
+ recoveryState: RecoveryState.IDLE,
410
+ currentTaskIndex: 0,
411
+ totalTasks: options.totalTasks || 0,
412
+ completedTasks: [],
413
+ restartCount: 0,
414
+ startTime: now,
415
+ lastTransitionTime: now,
416
+ pipelineBranch: options.pipelineBranch,
417
+ worktreeDir: options.worktreeDir,
418
+ metadata: options.metadata,
419
+ };
420
+
421
+ this.contexts.set(laneName, context);
422
+ this.history.set(laneName, []);
423
+
424
+ this.log(`[${laneName}] Registered (runId: ${runId}, tasks: ${options.totalTasks || 0})`);
425
+
426
+ return context;
427
+ }
428
+
429
+ /**
430
+ * Lane 해제
431
+ */
432
+ unregisterLane(laneName: string): void {
433
+ this.contexts.delete(laneName);
434
+ this.history.delete(laneName);
435
+ this.log(`[${laneName}] Unregistered`);
436
+ }
437
+
438
+ /**
439
+ * Lane 컨텍스트 조회
440
+ */
441
+ getContext(laneName: string): LaneStateContext | undefined {
442
+ return this.contexts.get(laneName);
443
+ }
444
+
445
+ /**
446
+ * 모든 Lane 컨텍스트 조회
447
+ */
448
+ getAllContexts(): Map<string, LaneStateContext> {
449
+ return new Map(this.contexts);
450
+ }
451
+
452
+ // --------------------------------------------------------------------------
453
+ // State Transitions
454
+ // --------------------------------------------------------------------------
455
+
456
+ /**
457
+ * 상태 전이 실행
458
+ */
459
+ transition(
460
+ laneName: string,
461
+ trigger: StateTransitionTrigger,
462
+ options: {
463
+ error?: string;
464
+ force?: boolean;
465
+ } = {}
466
+ ): StateTransitionResult {
467
+ const context = this.contexts.get(laneName);
468
+
469
+ if (!context) {
470
+ return {
471
+ success: false,
472
+ fromState: LanePrimaryState.PENDING,
473
+ toState: LanePrimaryState.PENDING,
474
+ trigger,
475
+ error: `Lane not found: ${laneName}`,
476
+ };
477
+ }
478
+
479
+ const currentState = context.primaryState;
480
+
481
+ // 전이 규칙 찾기
482
+ const rule = this.findTransitionRule(currentState, trigger);
483
+
484
+ if (!rule && !options.force) {
485
+ const result: StateTransitionResult = {
486
+ success: false,
487
+ fromState: currentState,
488
+ toState: currentState,
489
+ trigger,
490
+ error: `Invalid transition: ${currentState} + ${trigger}`,
491
+ };
492
+
493
+ this.emitTransitionFailed(laneName, context, trigger, result.error!);
494
+
495
+ return result;
496
+ }
497
+
498
+ const targetState = rule?.to || currentState;
499
+
500
+ // Guard 체크
501
+ if (rule?.guard && !rule.guard(context)) {
502
+ const result: StateTransitionResult = {
503
+ success: false,
504
+ fromState: currentState,
505
+ toState: currentState,
506
+ trigger,
507
+ error: 'Transition guard failed',
508
+ };
509
+
510
+ this.emitTransitionFailed(laneName, context, trigger, result.error!);
511
+
512
+ return result;
513
+ }
514
+
515
+ // 상태 전이 실행
516
+ const previousState = context.primaryState;
517
+ context.primaryState = targetState;
518
+ context.lastTransitionTime = Date.now();
519
+
520
+ // 오류 정보 업데이트
521
+ if (options.error) {
522
+ context.lastError = options.error;
523
+ }
524
+
525
+ // 종료 상태 처리
526
+ if (this.isTerminalState(targetState)) {
527
+ context.endTime = Date.now();
528
+ }
529
+
530
+ // 부 상태 리셋 (필요한 경우)
531
+ if (this.shouldResetSubState(previousState, targetState)) {
532
+ context.subState = LaneSubState.NONE;
533
+ }
534
+
535
+ // 히스토리 기록
536
+ const historyEntry = {
537
+ timestamp: Date.now(),
538
+ fromState: previousState,
539
+ toState: targetState,
540
+ trigger,
541
+ };
542
+ this.history.get(laneName)?.push(historyEntry);
543
+
544
+ // 콜백 및 이벤트 발행
545
+ const result: StateTransitionResult = {
546
+ success: true,
547
+ fromState: previousState,
548
+ toState: targetState,
549
+ trigger,
550
+ };
551
+
552
+ rule?.onTransition?.(context);
553
+ this.notifyTransition(laneName, result);
554
+ this.emitTransition(laneName, context, previousState, trigger);
555
+
556
+ this.log(`[${laneName}] ${previousState} → ${targetState} (trigger: ${trigger})`);
557
+
558
+ return result;
559
+ }
560
+
561
+ /**
562
+ * 전이 규칙 찾기
563
+ */
564
+ private findTransitionRule(currentState: LanePrimaryState, trigger: StateTransitionTrigger): TransitionRule | undefined {
565
+ return TRANSITION_RULES.find(rule => {
566
+ const fromStates = Array.isArray(rule.from) ? rule.from : [rule.from];
567
+ return fromStates.includes(currentState) && rule.trigger === trigger;
568
+ });
569
+ }
570
+
571
+ /**
572
+ * 종료 상태인지 확인
573
+ */
574
+ private isTerminalState(state: LanePrimaryState): boolean {
575
+ return [
576
+ LanePrimaryState.COMPLETED,
577
+ LanePrimaryState.FAILED,
578
+ LanePrimaryState.ABORTED,
579
+ ].includes(state);
580
+ }
581
+
582
+ /**
583
+ * 부 상태 리셋 필요 여부
584
+ */
585
+ private shouldResetSubState(from: LanePrimaryState, to: LanePrimaryState): boolean {
586
+ // RUNNING을 벗어날 때 부 상태 리셋
587
+ return from === LanePrimaryState.RUNNING && to !== LanePrimaryState.RUNNING;
588
+ }
589
+
590
+ // --------------------------------------------------------------------------
591
+ // Sub-State Management
592
+ // --------------------------------------------------------------------------
593
+
594
+ /**
595
+ * 부 상태 설정
596
+ */
597
+ setSubState(laneName: string, subState: LaneSubState): boolean {
598
+ const context = this.contexts.get(laneName);
599
+ if (!context) return false;
600
+
601
+ const previousSubState = context.subState;
602
+ context.subState = subState;
603
+
604
+ this.log(`[${laneName}] SubState: ${previousSubState} → ${subState}`);
605
+
606
+ return true;
607
+ }
608
+
609
+ /**
610
+ * 부 상태 조회
611
+ */
612
+ getSubState(laneName: string): LaneSubState {
613
+ return this.contexts.get(laneName)?.subState || LaneSubState.NONE;
614
+ }
615
+
616
+ // --------------------------------------------------------------------------
617
+ // Recovery State Management
618
+ // --------------------------------------------------------------------------
619
+
620
+ /**
621
+ * 복구 상태 설정
622
+ */
623
+ setRecoveryState(laneName: string, recoveryState: RecoveryState): boolean {
624
+ const context = this.contexts.get(laneName);
625
+ if (!context) return false;
626
+
627
+ const previousState = context.recoveryState;
628
+ context.recoveryState = recoveryState;
629
+
630
+ this.log(`[${laneName}] RecoveryState: ${previousState} → ${recoveryState}`);
631
+
632
+ return true;
633
+ }
634
+
635
+ /**
636
+ * 복구 상태 조회
637
+ */
638
+ getRecoveryState(laneName: string): RecoveryState {
639
+ return this.contexts.get(laneName)?.recoveryState || RecoveryState.IDLE;
640
+ }
641
+
642
+ /**
643
+ * 재시작 횟수 증가
644
+ */
645
+ incrementRestartCount(laneName: string): number {
646
+ const context = this.contexts.get(laneName);
647
+ if (!context) return 0;
648
+
649
+ context.restartCount++;
650
+ return context.restartCount;
651
+ }
652
+
653
+ // --------------------------------------------------------------------------
654
+ // Context Updates
655
+ // --------------------------------------------------------------------------
656
+
657
+ /**
658
+ * Task 진행 상황 업데이트
659
+ */
660
+ updateTaskProgress(
661
+ laneName: string,
662
+ taskIndex: number,
663
+ taskName?: string,
664
+ taskBranch?: string
665
+ ): boolean {
666
+ const context = this.contexts.get(laneName);
667
+ if (!context) return false;
668
+
669
+ context.currentTaskIndex = taskIndex;
670
+ if (taskName) context.currentTaskName = taskName;
671
+ if (taskBranch) context.taskBranch = taskBranch;
672
+
673
+ return true;
674
+ }
675
+
676
+ /**
677
+ * Task 완료 기록
678
+ */
679
+ recordTaskCompletion(laneName: string, taskName: string): boolean {
680
+ const context = this.contexts.get(laneName);
681
+ if (!context) return false;
682
+
683
+ if (!context.completedTasks.includes(taskName)) {
684
+ context.completedTasks.push(taskName);
685
+ }
686
+
687
+ return true;
688
+ }
689
+
690
+ /**
691
+ * 대기 중인 의존성 설정
692
+ */
693
+ setWaitingFor(laneName: string, dependencies: string[]): boolean {
694
+ const context = this.contexts.get(laneName);
695
+ if (!context) return false;
696
+
697
+ context.waitingFor = dependencies;
698
+ return true;
699
+ }
700
+
701
+ /**
702
+ * 오류 설정
703
+ */
704
+ setError(laneName: string, error: string): boolean {
705
+ const context = this.contexts.get(laneName);
706
+ if (!context) return false;
707
+
708
+ context.lastError = error;
709
+ return true;
710
+ }
711
+
712
+ /**
713
+ * 프로세스 ID 설정
714
+ */
715
+ setPid(laneName: string, pid: number): boolean {
716
+ const context = this.contexts.get(laneName);
717
+ if (!context) return false;
718
+
719
+ context.pid = pid;
720
+ return true;
721
+ }
722
+
723
+ /**
724
+ * 채팅 세션 ID 설정
725
+ */
726
+ setChatId(laneName: string, chatId: string): boolean {
727
+ const context = this.contexts.get(laneName);
728
+ if (!context) return false;
729
+
730
+ context.chatId = chatId;
731
+ return true;
732
+ }
733
+
734
+ /**
735
+ * 메타데이터 업데이트
736
+ */
737
+ updateMetadata(laneName: string, metadata: Record<string, any>): boolean {
738
+ const context = this.contexts.get(laneName);
739
+ if (!context) return false;
740
+
741
+ context.metadata = { ...context.metadata, ...metadata };
742
+ return true;
743
+ }
744
+
745
+ // --------------------------------------------------------------------------
746
+ // State Persistence
747
+ // --------------------------------------------------------------------------
748
+
749
+ /**
750
+ * 상태를 파일로 저장
751
+ */
752
+ persistState(laneName: string, stateDir: string): boolean {
753
+ const context = this.contexts.get(laneName);
754
+ if (!context) return false;
755
+
756
+ const statePath = safeJoin(stateDir, 'state.json');
757
+
758
+ // LaneState 형식으로 변환 (기존 호환성)
759
+ const laneState = this.contextToLaneState(context);
760
+
761
+ try {
762
+ const stateParent = require('path').dirname(statePath);
763
+ if (!fs.existsSync(stateParent)) {
764
+ fs.mkdirSync(stateParent, { recursive: true });
765
+ }
766
+
767
+ const tempPath = `${statePath}.tmp.${process.pid}`;
768
+ fs.writeFileSync(tempPath, JSON.stringify(laneState, null, 2), 'utf8');
769
+ fs.renameSync(tempPath, statePath);
770
+
771
+ events.emit(StateEventType.PERSISTED as any, {
772
+ laneName,
773
+ filePath: statePath,
774
+ });
775
+
776
+ return true;
777
+ } catch (error: any) {
778
+ logger.error(`[StateMachine] Failed to persist state for ${laneName}: ${error.message}`);
779
+ return false;
780
+ }
781
+ }
782
+
783
+ /**
784
+ * 파일에서 상태 복구
785
+ */
786
+ restoreState(laneName: string, runId: string, stateDir: string): LaneStateContext | null {
787
+ const statePath = safeJoin(stateDir, 'state.json');
788
+
789
+ if (!fs.existsSync(statePath)) {
790
+ return null;
791
+ }
792
+
793
+ try {
794
+ const content = fs.readFileSync(statePath, 'utf8');
795
+ const laneState = JSON.parse(content);
796
+
797
+ // LaneState에서 컨텍스트로 변환
798
+ const context = this.laneStateToContext(laneName, runId, laneState);
799
+
800
+ this.contexts.set(laneName, context);
801
+ this.history.set(laneName, []);
802
+
803
+ events.emit(StateEventType.RESTORED as any, {
804
+ laneName,
805
+ filePath: statePath,
806
+ state: laneState,
807
+ });
808
+
809
+ this.log(`[${laneName}] State restored from ${statePath}`);
810
+
811
+ return context;
812
+ } catch (error: any) {
813
+ logger.error(`[StateMachine] Failed to restore state for ${laneName}: ${error.message}`);
814
+
815
+ events.emit(StateEventType.CORRUPTED as any, {
816
+ laneName,
817
+ filePath: statePath,
818
+ issues: [error.message],
819
+ });
820
+
821
+ return null;
822
+ }
823
+ }
824
+
825
+ /**
826
+ * 컨텍스트를 기존 LaneState 형식으로 변환
827
+ */
828
+ private contextToLaneState(context: LaneStateContext): Record<string, any> {
829
+ return {
830
+ label: context.laneName,
831
+ status: context.primaryState,
832
+ currentTaskIndex: context.currentTaskIndex,
833
+ totalTasks: context.totalTasks,
834
+ worktreeDir: context.worktreeDir || null,
835
+ pipelineBranch: context.pipelineBranch || null,
836
+ startTime: context.startTime,
837
+ endTime: context.endTime || null,
838
+ error: context.lastError || null,
839
+ dependencyRequest: null,
840
+ updatedAt: Date.now(),
841
+ tasksFile: context.metadata?.tasksFile,
842
+ pid: context.pid,
843
+ completedTasks: context.completedTasks,
844
+ waitingFor: context.waitingFor,
845
+ chatId: context.chatId,
846
+ // 확장 필드
847
+ subState: context.subState,
848
+ recoveryState: context.recoveryState,
849
+ restartCount: context.restartCount,
850
+ currentTaskName: context.currentTaskName,
851
+ taskBranch: context.taskBranch,
852
+ };
853
+ }
854
+
855
+ /**
856
+ * 기존 LaneState에서 컨텍스트로 변환
857
+ */
858
+ private laneStateToContext(
859
+ laneName: string,
860
+ runId: string,
861
+ laneState: Record<string, any>
862
+ ): LaneStateContext {
863
+ return {
864
+ laneName,
865
+ runId,
866
+ primaryState: (laneState.status as LanePrimaryState) || LanePrimaryState.PENDING,
867
+ subState: (laneState.subState as LaneSubState) || LaneSubState.NONE,
868
+ recoveryState: (laneState.recoveryState as RecoveryState) || RecoveryState.IDLE,
869
+ currentTaskIndex: laneState.currentTaskIndex || 0,
870
+ totalTasks: laneState.totalTasks || 0,
871
+ completedTasks: laneState.completedTasks || [],
872
+ currentTaskName: laneState.currentTaskName,
873
+ waitingFor: laneState.waitingFor,
874
+ lastError: laneState.error,
875
+ restartCount: laneState.restartCount || 0,
876
+ startTime: laneState.startTime || Date.now(),
877
+ endTime: laneState.endTime,
878
+ lastTransitionTime: laneState.updatedAt || Date.now(),
879
+ worktreeDir: laneState.worktreeDir,
880
+ pipelineBranch: laneState.pipelineBranch,
881
+ taskBranch: laneState.taskBranch,
882
+ pid: laneState.pid,
883
+ chatId: laneState.chatId,
884
+ metadata: {
885
+ tasksFile: laneState.tasksFile,
886
+ },
887
+ };
888
+ }
889
+
890
+ // --------------------------------------------------------------------------
891
+ // Event Handling
892
+ // --------------------------------------------------------------------------
893
+
894
+ /**
895
+ * 상태 전이 콜백 등록
896
+ */
897
+ onTransition(callback: (laneName: string, result: StateTransitionResult) => void): () => void {
898
+ this.onTransitionCallbacks.push(callback);
899
+
900
+ return () => {
901
+ const index = this.onTransitionCallbacks.indexOf(callback);
902
+ if (index > -1) {
903
+ this.onTransitionCallbacks.splice(index, 1);
904
+ }
905
+ };
906
+ }
907
+
908
+ /**
909
+ * 전이 콜백 실행
910
+ */
911
+ private notifyTransition(laneName: string, result: StateTransitionResult): void {
912
+ for (const callback of this.onTransitionCallbacks) {
913
+ try {
914
+ callback(laneName, result);
915
+ } catch (error) {
916
+ logger.error(`[StateMachine] Callback error: ${error}`);
917
+ }
918
+ }
919
+ }
920
+
921
+ /**
922
+ * 전이 이벤트 발행
923
+ */
924
+ private emitTransition(
925
+ laneName: string,
926
+ context: LaneStateContext,
927
+ fromState: LanePrimaryState,
928
+ trigger: StateTransitionTrigger
929
+ ): void {
930
+ const payload: StateTransitionPayload = {
931
+ laneName,
932
+ fromState,
933
+ toState: context.primaryState,
934
+ trigger,
935
+ timestamp: Date.now(),
936
+ };
937
+
938
+ events.emit(StateEventType.TRANSITION as any, payload);
939
+ }
940
+
941
+ /**
942
+ * 전이 실패 이벤트 발행
943
+ */
944
+ private emitTransitionFailed(
945
+ laneName: string,
946
+ context: LaneStateContext,
947
+ trigger: StateTransitionTrigger,
948
+ reason: string
949
+ ): void {
950
+ const payload: StateTransitionFailedPayload = {
951
+ laneName,
952
+ fromState: context.primaryState,
953
+ attemptedState: 'unknown',
954
+ trigger,
955
+ reason,
956
+ };
957
+
958
+ events.emit(StateEventType.TRANSITION_FAILED as any, payload);
959
+ }
960
+
961
+ // --------------------------------------------------------------------------
962
+ // Query Methods
963
+ // --------------------------------------------------------------------------
964
+
965
+ /**
966
+ * 특정 상태의 Lane들 조회
967
+ */
968
+ getLanesInState(state: LanePrimaryState): string[] {
969
+ const result: string[] = [];
970
+ for (const [laneName, context] of this.contexts) {
971
+ if (context.primaryState === state) {
972
+ result.push(laneName);
973
+ }
974
+ }
975
+ return result;
976
+ }
977
+
978
+ /**
979
+ * 활성 상태 Lane들 조회 (종료되지 않은)
980
+ */
981
+ getActiveLanes(): string[] {
982
+ const result: string[] = [];
983
+ for (const [laneName, context] of this.contexts) {
984
+ if (!this.isTerminalState(context.primaryState)) {
985
+ result.push(laneName);
986
+ }
987
+ }
988
+ return result;
989
+ }
990
+
991
+ /**
992
+ * 전이 히스토리 조회
993
+ */
994
+ getHistory(laneName: string): Array<{
995
+ timestamp: number;
996
+ fromState: LanePrimaryState;
997
+ toState: LanePrimaryState;
998
+ trigger: StateTransitionTrigger;
999
+ }> {
1000
+ return this.history.get(laneName) || [];
1001
+ }
1002
+
1003
+ /**
1004
+ * 전체 상태 요약 조회
1005
+ */
1006
+ getSummary(): Record<LanePrimaryState, number> {
1007
+ const summary: Record<string, number> = {};
1008
+
1009
+ for (const state of Object.values(LanePrimaryState)) {
1010
+ summary[state] = 0;
1011
+ }
1012
+
1013
+ for (const context of this.contexts.values()) {
1014
+ summary[context.primaryState]++;
1015
+ }
1016
+
1017
+ return summary as Record<LanePrimaryState, number>;
1018
+ }
1019
+
1020
+ // --------------------------------------------------------------------------
1021
+ // Utility
1022
+ // --------------------------------------------------------------------------
1023
+
1024
+ /**
1025
+ * 로깅
1026
+ */
1027
+ private log(message: string): void {
1028
+ if (this.verbose) {
1029
+ logger.debug(`[StateMachine] ${message}`);
1030
+ }
1031
+ }
1032
+
1033
+ /**
1034
+ * 디버그 모드 설정
1035
+ */
1036
+ setVerbose(verbose: boolean): void {
1037
+ this.verbose = verbose;
1038
+ }
1039
+
1040
+ /**
1041
+ * 디버그 덤프
1042
+ */
1043
+ dumpState(laneName: string): string {
1044
+ const context = this.contexts.get(laneName);
1045
+ if (!context) return `Lane not found: ${laneName}`;
1046
+
1047
+ return JSON.stringify({
1048
+ ...context,
1049
+ historyLength: this.history.get(laneName)?.length || 0,
1050
+ }, null, 2);
1051
+ }
1052
+ }
1053
+
1054
+ // ============================================================================
1055
+ // Convenience Functions
1056
+ // ============================================================================
1057
+
1058
+ /**
1059
+ * 싱글톤 인스턴스 획득
1060
+ */
1061
+ export function getStateMachine(): LaneStateMachine {
1062
+ return LaneStateMachine.getInstance();
1063
+ }
1064
+
1065
+ /**
1066
+ * 인스턴스 리셋 (테스트용)
1067
+ */
1068
+ export function resetStateMachine(): void {
1069
+ LaneStateMachine.resetInstance();
1070
+ }
1071
+
1072
+ /**
1073
+ * LanePrimaryState를 기존 LaneStatus로 변환 (호환성)
1074
+ */
1075
+ export function primaryStateToLaneStatus(state: LanePrimaryState): string {
1076
+ switch (state) {
1077
+ case LanePrimaryState.PENDING:
1078
+ case LanePrimaryState.INITIALIZING:
1079
+ return 'pending';
1080
+ case LanePrimaryState.RUNNING:
1081
+ return 'running';
1082
+ case LanePrimaryState.WAITING:
1083
+ return 'waiting';
1084
+ case LanePrimaryState.PAUSED:
1085
+ return 'paused';
1086
+ case LanePrimaryState.RECOVERING:
1087
+ return 'running'; // 복구 중은 running으로 표시
1088
+ case LanePrimaryState.COMPLETED:
1089
+ return 'completed';
1090
+ case LanePrimaryState.FAILED:
1091
+ case LanePrimaryState.ABORTED:
1092
+ return 'failed';
1093
+ default:
1094
+ return 'unknown';
1095
+ }
1096
+ }
1097
+