aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
435 lines (376 loc) • 12.4 kB
JavaScript
/**
* Checkpoint Manager - YOLO 모드 체크포인트 매니저
*
* @usage YOLO 모드 및 CLI 명령어에서 사용
* @used_by src/cli/index.js, src/cli/checkpoint-cli.js
* @commands aiwf checkpoint, aiwf-checkpoint
* @warning 삭제 금지 - YOLO 시스템 핵심 구성 요소
*
* 진행 상황을 저장하고 복구할 수 있는 시스템
* - 세션 상태 관리
* - 체크포인트 생성/복구
* - 진행률 추적
* - 복구 가능한 상태 유지
*/
import fs from 'fs/promises';
import path from 'path';
import { execSync } from 'child_process';
export class CheckpointManager {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.stateFile = path.join(projectRoot, '.aiwf', 'yolo-state.json');
this.checkpointDir = path.join(projectRoot, '.aiwf', 'checkpoints');
this.currentState = null;
}
/**
* 체크포인트 디렉토리 초기화
*/
async initialize() {
await fs.mkdir(this.checkpointDir, { recursive: true });
await this.loadState();
}
/**
* 현재 상태 로드
*/
async loadState() {
try {
const content = await fs.readFile(this.stateFile, 'utf-8');
this.currentState = JSON.parse(content);
} catch {
// 새로운 상태 생성
this.currentState = {
session_id: Date.now().toString(),
started_at: new Date().toISOString(),
sprint_id: null,
mode: null,
completed_tasks: [],
current_task: null,
checkpoints: [],
metrics: {
total_tasks: 0,
completed_tasks: 0,
failed_tasks: 0,
skipped_tasks: 0,
total_time: 0,
avg_task_time: 0
}
};
}
}
/**
* 상태 저장
*/
async saveState() {
await fs.writeFile(this.stateFile, JSON.stringify(this.currentState, null, 2));
}
/**
* YOLO 세션 시작
*/
async startSession(sprintId, mode = 'sprint') {
this.currentState = {
session_id: Date.now().toString(),
started_at: new Date().toISOString(),
sprint_id: sprintId,
mode: mode, // sprint, sprint-all, milestone-all
completed_tasks: [],
current_task: null,
checkpoints: [],
metrics: {
total_tasks: 0,
completed_tasks: 0,
failed_tasks: 0,
skipped_tasks: 0,
total_time: 0,
avg_task_time: 0
},
git_info: this.getGitInfo()
};
await this.saveState();
await this.createCheckpoint('session_start');
}
/**
* Git 정보 수집
*/
getGitInfo() {
try {
return {
branch: execSync('git branch --show-current', { encoding: 'utf8' }).trim(),
commit: execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(),
status: execSync('git status --porcelain', { encoding: 'utf8' }).trim()
};
} catch {
return null;
}
}
/**
* 체크포인트 생성
*/
async createCheckpoint(type = 'auto', metadata = {}) {
const checkpointId = `cp_${Date.now()}`;
const checkpoint = {
id: checkpointId,
type: type, // session_start, task_complete, sprint_complete, auto, manual
created_at: new Date().toISOString(),
state_snapshot: { ...this.currentState },
git_info: this.getGitInfo(),
metadata: metadata
};
// 체크포인트 파일로 저장
const checkpointPath = path.join(this.checkpointDir, `${checkpointId}.json`);
await fs.writeFile(checkpointPath, JSON.stringify(checkpoint, null, 2));
// 상태에 체크포인트 추가
this.currentState.checkpoints.push({
id: checkpointId,
type: type,
created_at: checkpoint.created_at,
metadata: metadata
});
await this.saveState();
return checkpointId;
}
/**
* 태스크 시작
*/
async startTask(taskId, taskInfo = {}) {
this.currentState.current_task = {
id: taskId,
started_at: new Date().toISOString(),
info: taskInfo,
attempts: 1
};
await this.saveState();
}
/**
* 태스크 완료
*/
async completeTask(taskId, result = {}) {
if (this.currentState.current_task?.id !== taskId) {
throw new Error(`현재 태스크(${this.currentState.current_task?.id})와 완료하려는 태스크(${taskId})가 일치하지 않습니다.`);
}
const taskDuration = Date.now() - new Date(this.currentState.current_task.started_at).getTime();
this.currentState.completed_tasks.push({
id: taskId,
completed_at: new Date().toISOString(),
duration: taskDuration,
result: result,
attempts: this.currentState.current_task.attempts
});
// 메트릭 업데이트
this.currentState.metrics.completed_tasks++;
this.currentState.metrics.total_time += taskDuration;
this.currentState.metrics.avg_task_time =
this.currentState.metrics.total_time / this.currentState.metrics.completed_tasks;
this.currentState.current_task = null;
await this.saveState();
// 일정 간격으로 자동 체크포인트
if (this.currentState.completed_tasks.length % 5 === 0) {
await this.createCheckpoint('auto', {
tasks_completed: this.currentState.completed_tasks.length
});
}
}
/**
* 태스크 실패
*/
async failTask(taskId, error = {}) {
if (this.currentState.current_task?.id !== taskId) {
return;
}
this.currentState.metrics.failed_tasks++;
// 재시도 가능 여부 확인
if (this.currentState.current_task.attempts < 3) {
this.currentState.current_task.attempts++;
this.currentState.current_task.last_error = error;
await this.saveState();
return { retry: true, attempts: this.currentState.current_task.attempts };
}
// 최종 실패
this.currentState.current_task = null;
await this.saveState();
return { retry: false, final_failure: true };
}
/**
* 태스크 스킵
*/
async skipTask(taskId, reason = '') {
this.currentState.metrics.skipped_tasks++;
this.currentState.current_task = null;
await this.saveState();
}
/**
* 체크포인트에서 복구
*/
async restoreFromCheckpoint(checkpointId) {
const checkpointPath = path.join(this.checkpointDir, `${checkpointId}.json`);
try {
const content = await fs.readFile(checkpointPath, 'utf-8');
const checkpoint = JSON.parse(content);
// Git 상태 확인
const currentGitInfo = this.getGitInfo();
if (checkpoint.git_info && currentGitInfo) {
console.log(`⚠️ Git 상태 차이:`);
console.log(` 체크포인트 브랜치: ${checkpoint.git_info.branch}`);
console.log(` 현재 브랜치: ${currentGitInfo.branch}`);
if (checkpoint.git_info.branch !== currentGitInfo.branch) {
console.log(`브랜치 전환: git checkout ${checkpoint.git_info.branch}`);
}
}
// 상태 복원
this.currentState = checkpoint.state_snapshot;
this.currentState.restored_from = checkpointId;
this.currentState.restored_at = new Date().toISOString();
await this.saveState();
return {
success: true,
checkpoint: checkpoint,
tasks_to_resume: this.getResumableTasks()
};
} catch (error) {
throw new Error(`체크포인트 복원 실패: ${error.message}`);
}
}
/**
* 재개 가능한 태스크 목록
*/
getResumableTasks() {
const completedIds = this.currentState.completed_tasks.map(t => t.id);
return {
completed: completedIds,
current: this.currentState.current_task?.id,
next_task_hint: this.currentState.current_task ?
`마지막 작업: ${this.currentState.current_task.id}` :
`완료된 태스크 수: ${completedIds.length}`
};
}
/**
* 진행 상황 리포트
*/
async generateProgressReport() {
const report = {
session: {
id: this.currentState.session_id,
started: this.currentState.started_at,
sprint: this.currentState.sprint_id,
mode: this.currentState.mode
},
progress: {
completed: this.currentState.completed_tasks.length,
failed: this.currentState.metrics.failed_tasks,
skipped: this.currentState.metrics.skipped_tasks,
current: this.currentState.current_task?.id || 'none'
},
performance: {
total_time: this.formatDuration(this.currentState.metrics.total_time),
avg_task_time: this.formatDuration(this.currentState.metrics.avg_task_time),
success_rate: this.currentState.metrics.total_tasks > 0 ?
(this.currentState.metrics.completed_tasks / this.currentState.metrics.total_tasks * 100).toFixed(1) + '%' :
'N/A'
},
checkpoints: this.currentState.checkpoints.map(cp => ({
id: cp.id,
type: cp.type,
created: cp.created_at,
metadata: cp.metadata
})),
recommendations: this.generateRecommendations()
};
return report;
}
/**
* 시간 포맷팅
*/
formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}시간 ${minutes % 60}분`;
} else if (minutes > 0) {
return `${minutes}분 ${seconds % 60}초`;
} else {
return `${seconds}초`;
}
}
/**
* 권장사항 생성
*/
generateRecommendations() {
const recommendations = [];
// 실패율이 높은 경우
if (this.currentState.metrics.failed_tasks > this.currentState.metrics.completed_tasks * 0.2) {
recommendations.push('실패율이 높습니다. 태스크 범위를 재검토하세요.');
}
// 평균 시간이 긴 경우
if (this.currentState.metrics.avg_task_time > 30 * 60 * 1000) { // 30분
recommendations.push('태스크당 평균 시간이 깁니다. 더 작은 단위로 분할을 고려하세요.');
}
// 체크포인트가 없는 경우
if (this.currentState.checkpoints.length === 0 && this.currentState.completed_tasks.length > 10) {
recommendations.push('체크포인트를 생성하여 진행 상황을 보호하세요.');
}
return recommendations;
}
/**
* 세션 종료
*/
async endSession(summary = {}) {
this.currentState.ended_at = new Date().toISOString();
this.currentState.summary = summary;
await this.createCheckpoint('session_end', summary);
await this.saveState();
// 최종 리포트 생성
const finalReport = await this.generateProgressReport();
// 리포트 파일로 저장
const reportPath = path.join(
this.checkpointDir,
`session_${this.currentState.session_id}_report.json`
);
await fs.writeFile(reportPath, JSON.stringify(finalReport, null, 2));
return finalReport;
}
/**
* 체크포인트 목록
*/
async listCheckpoints() {
try {
const files = await fs.readdir(this.checkpointDir);
const checkpoints = [];
for (const file of files) {
if (file.startsWith('cp_') && file.endsWith('.json')) {
const content = await fs.readFile(
path.join(this.checkpointDir, file),
'utf-8'
);
const checkpoint = JSON.parse(content);
checkpoints.push({
id: checkpoint.id,
type: checkpoint.type,
created: checkpoint.created_at,
tasks_completed: checkpoint.state_snapshot.completed_tasks.length,
metadata: checkpoint.metadata
});
}
}
return checkpoints.sort((a, b) =>
new Date(b.created).getTime() - new Date(a.created).getTime()
);
} catch {
return [];
}
}
/**
* 정리 - 오래된 체크포인트 삭제
*/
async cleanup(keepLast = 10) {
const checkpoints = await this.listCheckpoints();
if (checkpoints.length <= keepLast) {
return;
}
const toDelete = checkpoints.slice(keepLast);
for (const cp of toDelete) {
const filePath = path.join(this.checkpointDir, `${cp.id}.json`);
await fs.unlink(filePath).catch(() => {});
}
}
}
export default CheckpointManager;