@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,1011 @@
1
+ /**
2
+ * Git Lifecycle Manager
3
+ *
4
+ * Git 파이프라인의 생명주기를 명확하게 관리합니다.
5
+ *
6
+ * 작업 흐름:
7
+ * 1. 작업 시작 (startWork): 브랜치 생성 및 체크아웃
8
+ * 2. 작업 중 (saveProgress): 주기적 커밋 (선택적)
9
+ * 3. 작업 종료 (finalizeWork): 남은 변경사항 커밋 및 푸시
10
+ * 4. 머지 (mergeToTarget): 다음 단계로 머지
11
+ *
12
+ * 생명주기 상태:
13
+ * - IDLE: 초기 상태 / 작업 종료 후
14
+ * - PREPARING: 브랜치 준비 중
15
+ * - WORKING: 작업 진행 중
16
+ * - COMMITTING: 커밋 중
17
+ * - PUSHING: 푸시 중
18
+ * - MERGING: 머지 중
19
+ * - ERROR: 오류 발생
20
+ */
21
+
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+
25
+ import * as git from '../utils/git';
26
+ import * as logger from '../utils/logger';
27
+ import { events } from '../utils/events';
28
+ import { safeJoin } from '../utils/path';
29
+ import { GitEventType } from '../types/event-categories';
30
+
31
+ // ============================================================================
32
+ // Types & Enums
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Git 작업 생명주기 상태
37
+ */
38
+ export enum GitLifecyclePhase {
39
+ /** 대기 상태 */
40
+ IDLE = 'IDLE',
41
+ /** 브랜치/워크트리 준비 중 */
42
+ PREPARING = 'PREPARING',
43
+ /** 작업 진행 중 */
44
+ WORKING = 'WORKING',
45
+ /** 커밋 중 */
46
+ COMMITTING = 'COMMITTING',
47
+ /** 푸시 중 */
48
+ PUSHING = 'PUSHING',
49
+ /** 머지 중 */
50
+ MERGING = 'MERGING',
51
+ /** 오류 발생 */
52
+ ERROR = 'ERROR',
53
+ }
54
+
55
+ /**
56
+ * 브랜치 유형
57
+ */
58
+ export enum BranchType {
59
+ /** Pipeline 브랜치 (Lane당 하나) */
60
+ PIPELINE = 'pipeline',
61
+ /** Task 브랜치 (Task당 하나) */
62
+ TASK = 'task',
63
+ /** Flow 브랜치 (최종 결과) */
64
+ FLOW = 'flow',
65
+ }
66
+
67
+ /**
68
+ * 작업 시작 옵션
69
+ */
70
+ export interface StartWorkOptions {
71
+ /** 워크트리 디렉토리 */
72
+ worktreeDir: string;
73
+ /** 생성할 브랜치 이름 */
74
+ branchName: string;
75
+ /** 베이스 브랜치 */
76
+ baseBranch: string;
77
+ /** Repository 루트 */
78
+ repoRoot: string;
79
+ /** Lane 이름 (로깅용) */
80
+ laneName?: string;
81
+ /** Task 이름 (로깅용) */
82
+ taskName?: string;
83
+ /** 브랜치 유형 */
84
+ branchType?: BranchType;
85
+ /** 의존성 브랜치들 (머지할 브랜치들) */
86
+ dependencyBranches?: string[];
87
+ }
88
+
89
+ /**
90
+ * 작업 종료 옵션
91
+ */
92
+ export interface FinalizeWorkOptions {
93
+ /** 워크트리 디렉토리 */
94
+ worktreeDir: string;
95
+ /** 현재 브랜치 이름 */
96
+ branchName: string;
97
+ /** 커밋 메시지 */
98
+ commitMessage?: string;
99
+ /** Lane 이름 (로깅용) */
100
+ laneName?: string;
101
+ /** Task 이름 (로깅용) */
102
+ taskName?: string;
103
+ /** 푸시 여부 (기본: true) */
104
+ push?: boolean;
105
+ /** 원격 이름 (기본: origin) */
106
+ remote?: string;
107
+ /** 변경사항 없으면 스킵 */
108
+ skipIfClean?: boolean;
109
+ }
110
+
111
+ /**
112
+ * 머지 옵션
113
+ */
114
+ export interface MergeOptions {
115
+ /** 워크트리 디렉토리 */
116
+ worktreeDir: string;
117
+ /** 소스 브랜치 */
118
+ sourceBranch: string;
119
+ /** 타겟 브랜치 */
120
+ targetBranch: string;
121
+ /** 커밋 메시지 */
122
+ commitMessage?: string;
123
+ /** Lane 이름 (로깅용) */
124
+ laneName?: string;
125
+ /** 머지 유형 */
126
+ mergeType?: 'task_to_pipeline' | 'dependency' | 'final';
127
+ /** 충돌 시 중단 */
128
+ abortOnConflict?: boolean;
129
+ /** 푸시 여부 */
130
+ push?: boolean;
131
+ }
132
+
133
+ /**
134
+ * 작업 결과
135
+ */
136
+ export interface GitOperationResult {
137
+ /** 성공 여부 */
138
+ success: boolean;
139
+ /** 오류 메시지 */
140
+ error?: string;
141
+ /** 생성된 커밋 해시 */
142
+ commitHash?: string;
143
+ /** 변경된 파일 수 */
144
+ filesChanged?: number;
145
+ /** 추가 정보 */
146
+ details?: Record<string, any>;
147
+ }
148
+
149
+ /**
150
+ * Lane별 Git 상태 추적
151
+ */
152
+ interface LaneGitState {
153
+ laneName: string;
154
+ phase: GitLifecyclePhase;
155
+ currentBranch: string | null;
156
+ pipelineBranch: string | null;
157
+ worktreeDir: string | null;
158
+ lastCommitHash: string | null;
159
+ uncommittedChanges: boolean;
160
+ lastOperation: string | null;
161
+ lastOperationTime: number | null;
162
+ error: string | null;
163
+ }
164
+
165
+ // ============================================================================
166
+ // Git Lifecycle Manager
167
+ // ============================================================================
168
+
169
+ /**
170
+ * Git 파이프라인 생명주기 관리자
171
+ *
172
+ * 사용 예:
173
+ * ```typescript
174
+ * const gitManager = GitLifecycleManager.getInstance();
175
+ *
176
+ * // 1. 작업 시작
177
+ * await gitManager.startWork({
178
+ * worktreeDir: '/path/to/worktree',
179
+ * branchName: 'cursorflow/task-1',
180
+ * baseBranch: 'main',
181
+ * repoRoot: '/path/to/repo',
182
+ * laneName: 'lane-1'
183
+ * });
184
+ *
185
+ * // 2. 작업 진행...
186
+ *
187
+ * // 3. 작업 종료 (모든 변경사항 커밋 및 푸시)
188
+ * await gitManager.finalizeWork({
189
+ * worktreeDir: '/path/to/worktree',
190
+ * branchName: 'cursorflow/task-1',
191
+ * commitMessage: 'feat: implement feature',
192
+ * laneName: 'lane-1'
193
+ * });
194
+ *
195
+ * // 4. 파이프라인 브랜치로 머지
196
+ * await gitManager.mergeToTarget({
197
+ * worktreeDir: '/path/to/worktree',
198
+ * sourceBranch: 'cursorflow/task-1',
199
+ * targetBranch: 'cursorflow/pipeline',
200
+ * laneName: 'lane-1',
201
+ * mergeType: 'task_to_pipeline'
202
+ * });
203
+ * ```
204
+ */
205
+ export class GitLifecycleManager {
206
+ private static instance: GitLifecycleManager | null = null;
207
+
208
+ /** Lane별 상태 추적 */
209
+ private laneStates: Map<string, LaneGitState> = new Map();
210
+
211
+ /** 디버그 모드 */
212
+ private verbose: boolean = false;
213
+
214
+ private constructor() {
215
+ this.verbose = process.env['DEBUG_GIT'] === 'true';
216
+ }
217
+
218
+ /**
219
+ * 싱글톤 인스턴스 획득
220
+ */
221
+ static getInstance(): GitLifecycleManager {
222
+ if (!GitLifecycleManager.instance) {
223
+ GitLifecycleManager.instance = new GitLifecycleManager();
224
+ }
225
+ return GitLifecycleManager.instance;
226
+ }
227
+
228
+ /**
229
+ * 인스턴스 리셋 (테스트용)
230
+ */
231
+ static resetInstance(): void {
232
+ GitLifecycleManager.instance = null;
233
+ }
234
+
235
+ // --------------------------------------------------------------------------
236
+ // Lane State Management
237
+ // --------------------------------------------------------------------------
238
+
239
+ /**
240
+ * Lane의 Git 상태 초기화
241
+ */
242
+ initializeLane(laneName: string, pipelineBranch: string, worktreeDir?: string): void {
243
+ this.laneStates.set(laneName, {
244
+ laneName,
245
+ phase: GitLifecyclePhase.IDLE,
246
+ currentBranch: null,
247
+ pipelineBranch,
248
+ worktreeDir: worktreeDir || null,
249
+ lastCommitHash: null,
250
+ uncommittedChanges: false,
251
+ lastOperation: null,
252
+ lastOperationTime: null,
253
+ error: null,
254
+ });
255
+
256
+ this.log(`[${laneName}] Git state initialized (pipeline: ${pipelineBranch})`);
257
+ }
258
+
259
+ /**
260
+ * Lane의 Git 상태 조회
261
+ */
262
+ getLaneState(laneName: string): LaneGitState | undefined {
263
+ return this.laneStates.get(laneName);
264
+ }
265
+
266
+ /**
267
+ * Lane 상태 업데이트
268
+ */
269
+ private updateLaneState(laneName: string, updates: Partial<LaneGitState>): void {
270
+ const state = this.laneStates.get(laneName);
271
+ if (state) {
272
+ Object.assign(state, updates, { lastOperationTime: Date.now() });
273
+ }
274
+ }
275
+
276
+ // --------------------------------------------------------------------------
277
+ // 1. Work Start (작업 시작)
278
+ // --------------------------------------------------------------------------
279
+
280
+ /**
281
+ * 작업 시작 - 브랜치 생성 및 체크아웃
282
+ *
283
+ * 1. 워크트리가 없으면 생성
284
+ * 2. 브랜치가 없으면 baseBranch에서 생성
285
+ * 3. 의존성 브랜치들 머지 (있는 경우)
286
+ * 4. 브랜치 체크아웃
287
+ */
288
+ async startWork(options: StartWorkOptions): Promise<GitOperationResult> {
289
+ const {
290
+ worktreeDir,
291
+ branchName,
292
+ baseBranch,
293
+ repoRoot,
294
+ laneName = 'unknown',
295
+ taskName,
296
+ branchType = BranchType.TASK,
297
+ dependencyBranches = [],
298
+ } = options;
299
+
300
+ this.updateLaneState(laneName, {
301
+ phase: GitLifecyclePhase.PREPARING,
302
+ lastOperation: 'startWork',
303
+ error: null,
304
+ });
305
+
306
+ try {
307
+ // 1. 워크트리 확인/생성
308
+ const worktreeResult = await this.ensureWorktree(worktreeDir, branchName, baseBranch, repoRoot, laneName);
309
+ if (!worktreeResult.success) {
310
+ return worktreeResult;
311
+ }
312
+
313
+ // 2. 브랜치 체크아웃
314
+ this.log(`[${laneName}] Checking out branch: ${branchName}`);
315
+ git.runGit(['checkout', branchName], { cwd: worktreeDir });
316
+
317
+ events.emit(GitEventType.BRANCH_CHECKED_OUT as any, {
318
+ laneName,
319
+ branchName,
320
+ });
321
+
322
+ // 3. 의존성 브랜치 머지 (있는 경우)
323
+ if (dependencyBranches.length > 0) {
324
+ for (const depBranch of dependencyBranches) {
325
+ const mergeResult = await this.mergeDependencyBranch(worktreeDir, depBranch, branchName, laneName);
326
+ if (!mergeResult.success) {
327
+ this.log(`[${laneName}] Warning: Failed to merge dependency ${depBranch}: ${mergeResult.error}`);
328
+ // 의존성 머지 실패는 경고만 하고 계속 진행
329
+ }
330
+ }
331
+ }
332
+
333
+ // 4. 상태 업데이트
334
+ this.updateLaneState(laneName, {
335
+ phase: GitLifecyclePhase.WORKING,
336
+ currentBranch: branchName,
337
+ worktreeDir,
338
+ uncommittedChanges: false,
339
+ });
340
+
341
+ this.log(`[${laneName}] Work started on branch: ${branchName}`);
342
+
343
+ return { success: true, details: { branchName, branchType } };
344
+
345
+ } catch (error: any) {
346
+ const errorMsg = `Failed to start work: ${error.message}`;
347
+ this.updateLaneState(laneName, {
348
+ phase: GitLifecyclePhase.ERROR,
349
+ error: errorMsg,
350
+ });
351
+
352
+ events.emit(GitEventType.ERROR as any, {
353
+ laneName,
354
+ operation: 'startWork',
355
+ error: errorMsg,
356
+ recoverable: true,
357
+ });
358
+
359
+ return { success: false, error: errorMsg };
360
+ }
361
+ }
362
+
363
+ /**
364
+ * 워크트리 확인 및 생성
365
+ */
366
+ private async ensureWorktree(
367
+ worktreeDir: string,
368
+ branchName: string,
369
+ baseBranch: string,
370
+ repoRoot: string,
371
+ laneName: string
372
+ ): Promise<GitOperationResult> {
373
+ const worktreeExists = fs.existsSync(worktreeDir);
374
+ const worktreeIsValid = worktreeExists && git.isValidWorktree(worktreeDir);
375
+
376
+ if (worktreeExists && !worktreeIsValid) {
377
+ this.log(`[${laneName}] Invalid worktree detected, cleaning up: ${worktreeDir}`);
378
+ try {
379
+ git.cleanupInvalidWorktreeDir(worktreeDir);
380
+ } catch (e: any) {
381
+ return { success: false, error: `Failed to cleanup invalid worktree: ${e.message}` };
382
+ }
383
+ }
384
+
385
+ if (!worktreeExists || !worktreeIsValid) {
386
+ this.log(`[${laneName}] Creating worktree: ${worktreeDir} (branch: ${branchName})`);
387
+
388
+ // 부모 디렉토리 생성
389
+ const worktreeParent = path.dirname(worktreeDir);
390
+ if (!fs.existsSync(worktreeParent)) {
391
+ fs.mkdirSync(worktreeParent, { recursive: true });
392
+ }
393
+
394
+ // 재시도 로직
395
+ let retries = 3;
396
+ let lastError: Error | null = null;
397
+
398
+ while (retries > 0) {
399
+ try {
400
+ await git.createWorktreeAsync(worktreeDir, branchName, {
401
+ baseBranch,
402
+ cwd: repoRoot,
403
+ });
404
+
405
+ events.emit(GitEventType.WORKTREE_CREATED as any, {
406
+ laneName,
407
+ worktreeDir,
408
+ branchName,
409
+ });
410
+
411
+ events.emit(GitEventType.BRANCH_CREATED as any, {
412
+ laneName,
413
+ branchName,
414
+ baseBranch,
415
+ worktreeDir,
416
+ });
417
+
418
+ return { success: true };
419
+ } catch (e: any) {
420
+ lastError = e;
421
+ retries--;
422
+ if (retries > 0) {
423
+ const delay = Math.floor(Math.random() * 1000) + 500;
424
+ this.log(`[${laneName}] Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
425
+ await new Promise(resolve => setTimeout(resolve, delay));
426
+ }
427
+ }
428
+ }
429
+
430
+ return { success: false, error: `Failed to create worktree after retries: ${lastError?.message}` };
431
+ }
432
+
433
+ // 기존 워크트리 재사용
434
+ this.log(`[${laneName}] Reusing existing worktree: ${worktreeDir}`);
435
+ return { success: true };
436
+ }
437
+
438
+ /**
439
+ * 의존성 브랜치 머지
440
+ */
441
+ private async mergeDependencyBranch(
442
+ worktreeDir: string,
443
+ depBranch: string,
444
+ currentBranch: string,
445
+ laneName: string
446
+ ): Promise<GitOperationResult> {
447
+ try {
448
+ // 원격에서 fetch
449
+ git.runGit(['fetch', 'origin', depBranch], { cwd: worktreeDir, silent: true });
450
+
451
+ const remoteBranch = `origin/${depBranch}`;
452
+
453
+ // 충돌 사전 체크
454
+ const conflictCheck = git.checkMergeConflict(remoteBranch, { cwd: worktreeDir });
455
+
456
+ if (conflictCheck.willConflict) {
457
+ events.emit(GitEventType.MERGE_CONFLICT as any, {
458
+ laneName,
459
+ sourceBranch: depBranch,
460
+ targetBranch: currentBranch,
461
+ conflictingFiles: conflictCheck.conflictingFiles,
462
+ preCheck: true,
463
+ });
464
+
465
+ return {
466
+ success: false,
467
+ error: `Merge conflict detected: ${conflictCheck.conflictingFiles.join(', ')}`
468
+ };
469
+ }
470
+
471
+ // 머지 실행
472
+ const mergeResult = git.safeMerge(remoteBranch, {
473
+ cwd: worktreeDir,
474
+ noFf: true,
475
+ message: `chore: merge dependency from ${depBranch}`,
476
+ abortOnConflict: true,
477
+ });
478
+
479
+ if (!mergeResult.success) {
480
+ return { success: false, error: mergeResult.error || 'Merge failed' };
481
+ }
482
+
483
+ events.emit(GitEventType.DEPENDENCY_SYNCED as any, {
484
+ laneName,
485
+ sourceBranch: depBranch,
486
+ targetBranch: currentBranch,
487
+ });
488
+
489
+ return { success: true };
490
+
491
+ } catch (error: any) {
492
+ return { success: false, error: error.message };
493
+ }
494
+ }
495
+
496
+ // --------------------------------------------------------------------------
497
+ // 2. Save Progress (진행 상황 저장 - 선택적)
498
+ // --------------------------------------------------------------------------
499
+
500
+ /**
501
+ * 진행 상황 저장 (중간 커밋)
502
+ *
503
+ * 장기 작업 중 주기적으로 호출하여 진행 상황을 저장할 수 있습니다.
504
+ */
505
+ async saveProgress(
506
+ worktreeDir: string,
507
+ message: string,
508
+ laneName: string = 'unknown'
509
+ ): Promise<GitOperationResult> {
510
+ try {
511
+ // 변경사항 확인
512
+ const status = git.runGit(['status', '--porcelain'], { cwd: worktreeDir });
513
+ if (!status.trim()) {
514
+ this.log(`[${laneName}] No changes to save`);
515
+ return { success: true, filesChanged: 0 };
516
+ }
517
+
518
+ this.updateLaneState(laneName, {
519
+ phase: GitLifecyclePhase.COMMITTING,
520
+ lastOperation: 'saveProgress',
521
+ });
522
+
523
+ // Stage all changes
524
+ git.runGit(['add', '-A'], { cwd: worktreeDir });
525
+
526
+ // Commit
527
+ git.runGit(['commit', '-m', message], { cwd: worktreeDir });
528
+
529
+ const commitHash = git.runGit(['rev-parse', 'HEAD'], { cwd: worktreeDir }).trim();
530
+
531
+ // 변경된 파일 수 계산
532
+ const filesChangedOutput = git.runGit(
533
+ ['diff', '--stat', 'HEAD~1', 'HEAD'],
534
+ { cwd: worktreeDir, silent: true }
535
+ );
536
+ const filesChanged = (filesChangedOutput.match(/\d+ file/g) || []).length || 1;
537
+
538
+ this.updateLaneState(laneName, {
539
+ phase: GitLifecyclePhase.WORKING,
540
+ lastCommitHash: commitHash,
541
+ uncommittedChanges: false,
542
+ });
543
+
544
+ events.emit(GitEventType.COMMITTED as any, {
545
+ laneName,
546
+ branchName: this.getLaneState(laneName)?.currentBranch || 'unknown',
547
+ commitHash,
548
+ message,
549
+ filesChanged,
550
+ });
551
+
552
+ this.log(`[${laneName}] Progress saved: ${commitHash.substring(0, 7)}`);
553
+
554
+ return { success: true, commitHash, filesChanged };
555
+
556
+ } catch (error: any) {
557
+ const errorMsg = `Failed to save progress: ${error.message}`;
558
+ this.updateLaneState(laneName, {
559
+ phase: GitLifecyclePhase.WORKING, // 복구 가능한 오류
560
+ error: errorMsg,
561
+ });
562
+
563
+ return { success: false, error: errorMsg };
564
+ }
565
+ }
566
+
567
+ // --------------------------------------------------------------------------
568
+ // 3. Finalize Work (작업 종료)
569
+ // --------------------------------------------------------------------------
570
+
571
+ /**
572
+ * 작업 종료 - 모든 변경사항 커밋 및 푸시
573
+ *
574
+ * 1. 남은 변경사항 모두 스테이징
575
+ * 2. 커밋 생성
576
+ * 3. 원격으로 푸시
577
+ */
578
+ async finalizeWork(options: FinalizeWorkOptions): Promise<GitOperationResult> {
579
+ const {
580
+ worktreeDir,
581
+ branchName,
582
+ commitMessage = 'chore: finalize work',
583
+ laneName = 'unknown',
584
+ taskName,
585
+ push = true,
586
+ remote = 'origin',
587
+ skipIfClean = false,
588
+ } = options;
589
+
590
+ try {
591
+ // 1. 변경사항 확인
592
+ const status = git.runGit(['status', '--porcelain'], { cwd: worktreeDir });
593
+ const hasChanges = !!status.trim();
594
+
595
+ if (!hasChanges && skipIfClean) {
596
+ this.log(`[${laneName}] No changes to finalize, skipping`);
597
+ return { success: true, filesChanged: 0 };
598
+ }
599
+
600
+ let commitHash: string | undefined;
601
+ let filesChanged = 0;
602
+
603
+ // 2. 변경사항이 있으면 커밋
604
+ if (hasChanges) {
605
+ this.updateLaneState(laneName, {
606
+ phase: GitLifecyclePhase.COMMITTING,
607
+ lastOperation: 'finalizeWork',
608
+ });
609
+
610
+ // Stage all changes
611
+ git.runGit(['add', '-A'], { cwd: worktreeDir });
612
+
613
+ // Commit
614
+ const fullMessage = taskName
615
+ ? `${commitMessage}\n\nTask: ${taskName}`
616
+ : commitMessage;
617
+
618
+ git.runGit(['commit', '-m', fullMessage], { cwd: worktreeDir });
619
+
620
+ commitHash = git.runGit(['rev-parse', 'HEAD'], { cwd: worktreeDir }).trim();
621
+
622
+ // 변경된 파일 수 계산
623
+ const filesChangedOutput = git.runGit(
624
+ ['diff', '--stat', 'HEAD~1', 'HEAD'],
625
+ { cwd: worktreeDir, silent: true }
626
+ );
627
+ filesChanged = (filesChangedOutput.match(/\d+ file/g) || []).length || 1;
628
+
629
+ events.emit(GitEventType.COMMITTED as any, {
630
+ laneName,
631
+ branchName,
632
+ commitHash,
633
+ message: commitMessage,
634
+ filesChanged,
635
+ });
636
+
637
+ this.log(`[${laneName}] Changes committed: ${commitHash.substring(0, 7)} (${filesChanged} files)`);
638
+ }
639
+
640
+ // 3. 푸시
641
+ if (push) {
642
+ this.updateLaneState(laneName, {
643
+ phase: GitLifecyclePhase.PUSHING,
644
+ });
645
+
646
+ try {
647
+ git.push(branchName, { cwd: worktreeDir, setUpstream: true });
648
+
649
+ events.emit(GitEventType.PUSHED as any, {
650
+ laneName,
651
+ branchName,
652
+ remote,
653
+ commitHash: commitHash || git.runGit(['rev-parse', 'HEAD'], { cwd: worktreeDir }).trim(),
654
+ });
655
+
656
+ this.log(`[${laneName}] Pushed to ${remote}/${branchName}`);
657
+
658
+ } catch (pushError: any) {
659
+ // 푸시 실패 처리
660
+ events.emit(GitEventType.PUSH_REJECTED as any, {
661
+ laneName,
662
+ branchName,
663
+ reason: pushError.message,
664
+ hint: 'Try pulling remote changes first',
665
+ });
666
+
667
+ // 푸시 실패는 별도 처리 가능하도록 세부 정보 반환
668
+ return {
669
+ success: false,
670
+ error: `Push failed: ${pushError.message}`,
671
+ commitHash,
672
+ filesChanged,
673
+ details: { pushFailed: true },
674
+ };
675
+ }
676
+ }
677
+
678
+ // 4. 상태 업데이트
679
+ this.updateLaneState(laneName, {
680
+ phase: GitLifecyclePhase.IDLE,
681
+ lastCommitHash: commitHash || this.getLaneState(laneName)?.lastCommitHash,
682
+ uncommittedChanges: false,
683
+ error: null,
684
+ });
685
+
686
+ return { success: true, commitHash, filesChanged };
687
+
688
+ } catch (error: any) {
689
+ const errorMsg = `Failed to finalize work: ${error.message}`;
690
+ this.updateLaneState(laneName, {
691
+ phase: GitLifecyclePhase.ERROR,
692
+ error: errorMsg,
693
+ });
694
+
695
+ events.emit(GitEventType.ERROR as any, {
696
+ laneName,
697
+ operation: 'finalizeWork',
698
+ error: errorMsg,
699
+ recoverable: true,
700
+ });
701
+
702
+ return { success: false, error: errorMsg };
703
+ }
704
+ }
705
+
706
+ // --------------------------------------------------------------------------
707
+ // 4. Merge to Target (머지)
708
+ // --------------------------------------------------------------------------
709
+
710
+ /**
711
+ * 타겟 브랜치로 머지
712
+ *
713
+ * 사용 사례:
714
+ * - Task → Pipeline 머지
715
+ * - 의존성 브랜치 머지
716
+ * - 최종 Flow 브랜치 생성
717
+ */
718
+ async mergeToTarget(options: MergeOptions): Promise<GitOperationResult> {
719
+ const {
720
+ worktreeDir,
721
+ sourceBranch,
722
+ targetBranch,
723
+ commitMessage,
724
+ laneName = 'unknown',
725
+ mergeType = 'task_to_pipeline',
726
+ abortOnConflict = true,
727
+ push = true,
728
+ } = options;
729
+
730
+ this.updateLaneState(laneName, {
731
+ phase: GitLifecyclePhase.MERGING,
732
+ lastOperation: 'mergeToTarget',
733
+ });
734
+
735
+ try {
736
+ // 1. 타겟 브랜치로 체크아웃
737
+ this.log(`[${laneName}] Checking out ${targetBranch} for merge`);
738
+ git.runGit(['checkout', targetBranch], { cwd: worktreeDir });
739
+
740
+ events.emit(GitEventType.MERGE_STARTED as any, {
741
+ laneName,
742
+ sourceBranch,
743
+ targetBranch,
744
+ mergeType,
745
+ });
746
+
747
+ // 2. 충돌 사전 체크
748
+ const conflictCheck = git.checkMergeConflict(sourceBranch, { cwd: worktreeDir });
749
+
750
+ if (conflictCheck.willConflict) {
751
+ events.emit(GitEventType.MERGE_CONFLICT as any, {
752
+ laneName,
753
+ sourceBranch,
754
+ targetBranch,
755
+ conflictingFiles: conflictCheck.conflictingFiles,
756
+ preCheck: true,
757
+ });
758
+
759
+ if (abortOnConflict) {
760
+ this.updateLaneState(laneName, {
761
+ phase: GitLifecyclePhase.ERROR,
762
+ error: `Merge conflict: ${conflictCheck.conflictingFiles.join(', ')}`,
763
+ });
764
+
765
+ return {
766
+ success: false,
767
+ error: `Merge conflict detected: ${conflictCheck.conflictingFiles.join(', ')}`,
768
+ details: { conflictingFiles: conflictCheck.conflictingFiles },
769
+ };
770
+ }
771
+ }
772
+
773
+ // 3. 머지 실행
774
+ const mergeMsg = commitMessage || `chore: merge ${sourceBranch} into ${targetBranch}`;
775
+
776
+ const mergeResult = git.safeMerge(sourceBranch, {
777
+ cwd: worktreeDir,
778
+ noFf: true,
779
+ message: mergeMsg,
780
+ abortOnConflict,
781
+ });
782
+
783
+ if (!mergeResult.success) {
784
+ if (mergeResult.conflict) {
785
+ events.emit(GitEventType.MERGE_CONFLICT as any, {
786
+ laneName,
787
+ sourceBranch,
788
+ targetBranch,
789
+ conflictingFiles: mergeResult.conflictingFiles,
790
+ preCheck: false,
791
+ });
792
+ }
793
+
794
+ this.updateLaneState(laneName, {
795
+ phase: GitLifecyclePhase.ERROR,
796
+ error: mergeResult.error || 'Merge failed',
797
+ });
798
+
799
+ return {
800
+ success: false,
801
+ error: mergeResult.error || 'Merge failed',
802
+ details: { conflictingFiles: mergeResult.conflictingFiles },
803
+ };
804
+ }
805
+
806
+ const mergeCommit = git.runGit(['rev-parse', 'HEAD'], { cwd: worktreeDir }).trim();
807
+
808
+ // 변경된 파일 수 계산
809
+ const stats = git.getLastOperationStats(worktreeDir);
810
+ const filesChangedMatch = stats?.match(/(\d+) file/);
811
+ const filesChanged = filesChangedMatch ? parseInt(filesChangedMatch[1]!, 10) : 0;
812
+
813
+ events.emit(GitEventType.MERGE_COMPLETED as any, {
814
+ laneName,
815
+ sourceBranch,
816
+ targetBranch,
817
+ mergeCommit,
818
+ filesChanged,
819
+ });
820
+
821
+ this.log(`[${laneName}] Merged ${sourceBranch} → ${targetBranch} (${mergeCommit.substring(0, 7)})`);
822
+
823
+ // 4. 푸시
824
+ if (push) {
825
+ this.updateLaneState(laneName, {
826
+ phase: GitLifecyclePhase.PUSHING,
827
+ });
828
+
829
+ try {
830
+ git.push(targetBranch, { cwd: worktreeDir, setUpstream: true });
831
+
832
+ events.emit(GitEventType.PUSHED as any, {
833
+ laneName,
834
+ branchName: targetBranch,
835
+ remote: 'origin',
836
+ commitHash: mergeCommit,
837
+ });
838
+
839
+ } catch (pushError: any) {
840
+ events.emit(GitEventType.PUSH_REJECTED as any, {
841
+ laneName,
842
+ branchName: targetBranch,
843
+ reason: pushError.message,
844
+ });
845
+
846
+ return {
847
+ success: false,
848
+ error: `Merge succeeded but push failed: ${pushError.message}`,
849
+ commitHash: mergeCommit,
850
+ filesChanged,
851
+ details: { pushFailed: true },
852
+ };
853
+ }
854
+ }
855
+
856
+ // 5. 상태 업데이트
857
+ this.updateLaneState(laneName, {
858
+ phase: GitLifecyclePhase.IDLE,
859
+ currentBranch: targetBranch,
860
+ lastCommitHash: mergeCommit,
861
+ error: null,
862
+ });
863
+
864
+ return { success: true, commitHash: mergeCommit, filesChanged };
865
+
866
+ } catch (error: any) {
867
+ const errorMsg = `Failed to merge: ${error.message}`;
868
+ this.updateLaneState(laneName, {
869
+ phase: GitLifecyclePhase.ERROR,
870
+ error: errorMsg,
871
+ });
872
+
873
+ events.emit(GitEventType.ERROR as any, {
874
+ laneName,
875
+ operation: 'mergeToTarget',
876
+ error: errorMsg,
877
+ recoverable: true,
878
+ });
879
+
880
+ return { success: false, error: errorMsg };
881
+ }
882
+ }
883
+
884
+ // --------------------------------------------------------------------------
885
+ // 5. Cleanup (정리)
886
+ // --------------------------------------------------------------------------
887
+
888
+ /**
889
+ * 워크트리 정리
890
+ */
891
+ async cleanupWorktree(worktreeDir: string, laneName: string = 'unknown'): Promise<GitOperationResult> {
892
+ try {
893
+ if (!fs.existsSync(worktreeDir)) {
894
+ return { success: true };
895
+ }
896
+
897
+ git.cleanupInvalidWorktreeDir(worktreeDir);
898
+
899
+ events.emit(GitEventType.WORKTREE_CLEANED as any, {
900
+ laneName,
901
+ worktreeDir,
902
+ });
903
+
904
+ this.log(`[${laneName}] Worktree cleaned: ${worktreeDir}`);
905
+
906
+ return { success: true };
907
+
908
+ } catch (error: any) {
909
+ return { success: false, error: `Failed to cleanup worktree: ${error.message}` };
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Lane 상태 정리
915
+ */
916
+ cleanupLane(laneName: string): void {
917
+ this.laneStates.delete(laneName);
918
+ this.log(`[${laneName}] Git state cleaned up`);
919
+ }
920
+
921
+ // --------------------------------------------------------------------------
922
+ // Utility Methods
923
+ // --------------------------------------------------------------------------
924
+
925
+ /**
926
+ * 현재 브랜치에 미커밋 변경사항이 있는지 확인
927
+ */
928
+ hasUncommittedChanges(worktreeDir: string): boolean {
929
+ try {
930
+ const status = git.runGit(['status', '--porcelain'], { cwd: worktreeDir });
931
+ return !!status.trim();
932
+ } catch {
933
+ return false;
934
+ }
935
+ }
936
+
937
+ /**
938
+ * 안전하게 모든 변경사항 커밋 (오류 무시)
939
+ */
940
+ async safeCommitAll(
941
+ worktreeDir: string,
942
+ message: string,
943
+ laneName: string = 'unknown'
944
+ ): Promise<GitOperationResult> {
945
+ try {
946
+ if (!this.hasUncommittedChanges(worktreeDir)) {
947
+ return { success: true, filesChanged: 0 };
948
+ }
949
+
950
+ git.runGit(['add', '-A'], { cwd: worktreeDir });
951
+ git.runGit(['commit', '-m', message, '--no-verify'], { cwd: worktreeDir });
952
+
953
+ const commitHash = git.runGit(['rev-parse', 'HEAD'], { cwd: worktreeDir }).trim();
954
+
955
+ return { success: true, commitHash };
956
+
957
+ } catch (error: any) {
958
+ // 오류 무시하고 반환
959
+ return { success: false, error: error.message };
960
+ }
961
+ }
962
+
963
+ /**
964
+ * 브랜치가 원격에 존재하는지 확인
965
+ */
966
+ branchExistsRemote(branchName: string, worktreeDir: string): boolean {
967
+ try {
968
+ git.runGit(['ls-remote', '--heads', 'origin', branchName], { cwd: worktreeDir });
969
+ return true;
970
+ } catch {
971
+ return false;
972
+ }
973
+ }
974
+
975
+ /**
976
+ * 로깅 헬퍼
977
+ */
978
+ private log(message: string): void {
979
+ if (this.verbose) {
980
+ logger.debug(`[GitLifecycle] ${message}`);
981
+ } else {
982
+ logger.info(`[GitLifecycle] ${message}`);
983
+ }
984
+ }
985
+
986
+ /**
987
+ * 디버그 모드 설정
988
+ */
989
+ setVerbose(verbose: boolean): void {
990
+ this.verbose = verbose;
991
+ }
992
+ }
993
+
994
+ // ============================================================================
995
+ // Convenience Functions
996
+ // ============================================================================
997
+
998
+ /**
999
+ * 싱글톤 인스턴스 획득
1000
+ */
1001
+ export function getGitLifecycleManager(): GitLifecycleManager {
1002
+ return GitLifecycleManager.getInstance();
1003
+ }
1004
+
1005
+ /**
1006
+ * 인스턴스 리셋 (테스트용)
1007
+ */
1008
+ export function resetGitLifecycleManager(): void {
1009
+ GitLifecycleManager.resetInstance();
1010
+ }
1011
+