From 6e7195547f348f3fa4316b306b782cbf84e63732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=84=B1?= <66245186+kys0213@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:43:12 +0900 Subject: [PATCH] docs(spec): promote v6 draft to current and archive v5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - spec/draft/ (v6) → spec/ (current) - spec/ (v5) → spec/archive/v5/ - v6 adds evaluator, lifecycle-hook, stagnation concerns - draft/ directory emptied for future use Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/{draft/DESIGN-v6.md => DESIGN.md} | 0 spec/README.md | 61 ++- spec/{ => archive/v5}/DESIGN-v5.md | 0 spec/archive/v5/README.md | 45 +++ .../v5}/concerns/agent-runtime.md | 0 .../v5}/concerns/agent-workspace.md | 57 +-- .../v5}/concerns/cli-reference.md | 0 .../v5}/concerns/cron-engine.md | 42 ++- .../v5}/concerns/cross-platform.md | 0 spec/archive/v5/concerns/daemon.md | 186 +++++++++ .../v5}/concerns/data-model.md | 161 +------- .../v5}/concerns/datasource.md | 136 ++++--- .../v5}/concerns/distribution.md | 0 .../v5}/concerns/queue-state-machine.md | 112 +----- spec/{draft => archive/v5}/flows/01-setup.md | 27 -- .../v5}/flows/02-spec-lifecycle.md | 60 +-- spec/archive/v5/flows/03-issue-pipeline.md | 100 +++++ spec/archive/v5/flows/04-failure-and-hitl.md | 128 +++++++ .../v5}/flows/05-monitoring.md | 71 ++-- spec/concerns/agent-workspace.md | 57 ++- spec/concerns/cron-engine.md | 42 +-- spec/concerns/daemon.md | 331 ++++++++++++---- spec/concerns/data-model.md | 161 +++++++- spec/concerns/datasource.md | 136 +++---- spec/{draft => }/concerns/evaluator.md | 0 spec/{draft => }/concerns/lifecycle-hook.md | 0 spec/concerns/queue-state-machine.md | 112 +++++- spec/{draft => }/concerns/stagnation.md | 0 spec/draft/.gitkeep | 0 spec/draft/README.md | 66 ---- spec/draft/concerns/daemon.md | 355 ------------------ spec/draft/flows/03-issue-pipeline.md | 147 -------- spec/draft/flows/04-failure-and-hitl.md | 236 ------------ spec/flows/01-setup.md | 27 ++ spec/flows/02-spec-lifecycle.md | 60 ++- spec/flows/03-issue-pipeline.md | 75 +++- spec/flows/04-failure-and-hitl.md | 180 +++++++-- spec/flows/05-monitoring.md | 71 ++-- 38 files changed, 1621 insertions(+), 1621 deletions(-) rename spec/{draft/DESIGN-v6.md => DESIGN.md} (100%) rename spec/{ => archive/v5}/DESIGN-v5.md (100%) create mode 100644 spec/archive/v5/README.md rename spec/{draft => archive/v5}/concerns/agent-runtime.md (100%) rename spec/{draft => archive/v5}/concerns/agent-workspace.md (79%) rename spec/{draft => archive/v5}/concerns/cli-reference.md (100%) rename spec/{draft => archive/v5}/concerns/cron-engine.md (62%) rename spec/{draft => archive/v5}/concerns/cross-platform.md (100%) create mode 100644 spec/archive/v5/concerns/daemon.md rename spec/{draft => archive/v5}/concerns/data-model.md (66%) rename spec/{draft => archive/v5}/concerns/datasource.md (66%) rename spec/{draft => archive/v5}/concerns/distribution.md (100%) rename spec/{draft => archive/v5}/concerns/queue-state-machine.md (68%) rename spec/{draft => archive/v5}/flows/01-setup.md (79%) rename spec/{draft => archive/v5}/flows/02-spec-lifecycle.md (65%) create mode 100644 spec/archive/v5/flows/03-issue-pipeline.md create mode 100644 spec/archive/v5/flows/04-failure-and-hitl.md rename spec/{draft => archive/v5}/flows/05-monitoring.md (80%) rename spec/{draft => }/concerns/evaluator.md (100%) rename spec/{draft => }/concerns/lifecycle-hook.md (100%) rename spec/{draft => }/concerns/stagnation.md (100%) create mode 100644 spec/draft/.gitkeep delete mode 100644 spec/draft/README.md delete mode 100644 spec/draft/concerns/daemon.md delete mode 100644 spec/draft/flows/03-issue-pipeline.md delete mode 100644 spec/draft/flows/04-failure-and-hitl.md diff --git a/spec/draft/DESIGN-v6.md b/spec/DESIGN.md similarity index 100% rename from spec/draft/DESIGN-v6.md rename to spec/DESIGN.md diff --git a/spec/README.md b/spec/README.md index 5d855f0..c94c5b5 100644 --- a/spec/README.md +++ b/spec/README.md @@ -1,22 +1,24 @@ -# Spec v5 Draft +# Spec v6 Draft -> **Date**: 2026-03-22 -> **Status**: Draft (reviewed) +> **Date**: 2026-04-04 +> **Status**: Draft > **구조**: 설계 개요 + 관심사별 상세 스펙 + 사용자 플로우 -## 핵심 변경 (v4 → v5) +## 핵심 변경 (v5 → v6) -- **Daemon = 상태 머신 + 실행기**: yaml에 정의된 prompt/script만 호출하는 단순 실행기로 축소 -- **Task trait 제거**: 5개 구현체 → prompt/script로 대체 -- **workspace = 1 repo**: 다른 DataSource 지원을 위한 추상화 -- **QueuePhase 8개**: Pending/Ready/Running/Completed/Done/HITL/Failed/Skipped -- **worktree는 인프라**: 항상 worktree에서 작업, Done 시 정리 -- **환경변수 최소화**: `WORK_ID` + `WORKTREE`만, 나머지는 `belt context` CLI -- **evaluate = CLI 도구 호출**: LLM이 `belt queue done/hitl` 실행 +- **LifecycleHook 분리**: on_done/on_fail/on_enter를 yaml script에서 `LifecycleHook` trait으로 분리. handler(작업)와 hook(반응) 관심사 분리. DataSource별 impl, workspace별 바인딩, lazy 로딩 +- **Daemon = CPU**: yaml script 실행기 → 상태 머신 CPU. hook 트리거만, 실행 책임은 Hook impl이 소유 +- **Stagnation Detection**: 실패 횟수가 아니라 실패 패턴(SPINNING, OSCILLATION)을 감지 +- **Daemon 모듈 분리**: 단일 daemon.rs → Advancer + Executor + HitlService + StagnationDetector 모듈 +- **Phase 전이 캡슐화**: `item.phase` 직접 대입 금지, `QueueItem::transit()` 메서드 강제 +- **ItemContext 확장**: `source_data: serde_json::Value` 추가 — 새 DataSource 추가 시 코어 변경 0 +- **hitl_terminal_action 타입 안전**: `Option` → `Option` +- **Dependency Gate DB 기반**: in-memory → DB 조회, 재시작 시 순서 보장 +- **Evaluator per-item 판정**: workspace 배치 → per-work_id 개별 LLM 판정 ## 설계 문서 -- **[DESIGN-v5.md](./DESIGN-v5.md)** — 설계 철학 + 전체 구조 개요 (간결) +- **[DESIGN-v6.md](./DESIGN-v6.md)** — 설계 철학 + 전체 구조 개요 (간결) ## 관심사별 상세 스펙 (concerns/) @@ -24,13 +26,19 @@ | 문서 | 설명 | |------|------| -| [QueuePhase 상태 머신](./concerns/queue-state-machine.md) | 8개 phase 전이 다이어그램, worktree 생명주기, on_fail 조건 | -| [Daemon](./concerns/daemon.md) | 실행 루프 의사코드, concurrency, graceful shutdown | -| [DataSource](./concerns/datasource.md) | 외부 시스템 추상화 trait + context CLI + 워크플로우 yaml | +| [QueuePhase 상태 머신](./concerns/queue-state-machine.md) | 8개 phase 전이, **전이 캡슐화**, worktree 생명주기, on_fail 조건 | +| [Daemon](./concerns/daemon.md) | **내부 모듈 구조**, 실행 루프, **DB dependency gate**, concurrency, graceful shutdown | +| [Evaluator](./concerns/evaluator.md) | **v6 신규** — Progressive Evaluation Pipeline, Evaluate before Execute | +| [Stagnation Detection](./concerns/stagnation.md) | **v6 신규** — 4가지 정체 패턴, 해시 기반 탐지 | +| [LifecycleHook](./concerns/lifecycle-hook.md) | **v6 신규** — 상태 전이 반응 trait, handler/hook 분리, lazy 로딩 | +| [DataSource](./concerns/datasource.md) | 외부 시스템 추상화 trait + **source_data** + 워크플로우 yaml | | [AgentRuntime](./concerns/agent-runtime.md) | LLM 실행 추상화 trait + Registry | -| [Agent 워크스페이스](./concerns/agent-workspace.md) | 대화형 에이전트 + evaluate + slash command | -| [Cron 엔진](./concerns/cron-engine.md) | 주기 실행 + 품질 루프 + force trigger | +| [Agent 워크스페이스](./concerns/agent-workspace.md) | 대화형 에이전트 + **per-item evaluate** + slash command | +| [Cron 엔진](./concerns/cron-engine.md) | 주기 실행 + 품질 루프 (evaluate는 Daemon tick으로 이동) | | [CLI 레퍼런스](./concerns/cli-reference.md) | 3-layer SSOT + `belt context` + 전체 커맨드 트리 | +| [Cross-Platform](./concerns/cross-platform.md) | OS 추상화 (ShellExecutor, DaemonNotifier) | +| [Distribution](./concerns/distribution.md) | 배포 전략 | +| [Data Model](./concerns/data-model.md) | SQLite 스키마, **StagnationPattern enum**, **EscalationAction FromStr**, **source_data** | ## 사용자 플로우 (flows/) @@ -40,6 +48,19 @@ |---|------|------| | 01 | [온보딩](./flows/01-setup.md) | workspace 등록 → 컨벤션 부트스트랩 | | 02 | [스펙 생명주기](./flows/02-spec-lifecycle.md) | 스펙 등록 → 이슈 분해 → 완료 판정 | -| 03 | [이슈 파이프라인](./flows/03-issue-pipeline.md) | handlers 실행 → evaluate → on_done | -| 04 | [실패 복구와 HITL](./flows/04-failure-and-hitl.md) | escalation → on_fail → 사람 개입 | -| 05 | [모니터링](./flows/05-monitoring.md) | TUI + CLI + /agent 시각화 | +| 03 | [이슈 파이프라인](./flows/03-issue-pipeline.md) | handlers 실행 → **stagnation detection** → evaluate → hook.on_done | +| 04 | [실패 복구와 HITL](./flows/04-failure-and-hitl.md) | **stagnation + lateral thinking** → escalation → hook 트리거 → 사람 개입 | +| 05 | [모니터링](./flows/05-monitoring.md) | TUI + CLI + /agent 시각화 + **stagnation 표시** | + +## 이슈 매핑 + +| 이슈 | 주요 반영 문서 | +|------|-------------| +| 신규 LifecycleHook 분리 | lifecycle-hook.md, datasource.md, daemon.md, DESIGN | +| #723 Stagnation/Oscillation 탐지 | stagnation.md, daemon.md, data-model.md, flow-04, DESIGN | +| #717 Daemon 내부 모듈 분리 | daemon.md, DESIGN | +| #718 Phase 전이 캡슐화 | queue-state-machine.md, data-model.md, DESIGN | +| #719 ItemContext source_data | data-model.md, datasource.md | +| #720 hitl_terminal_action 타입 | data-model.md, flow-04 | +| #721 Dependency Gate DB 기반 | daemon.md | +| #722 Evaluator per-item 판정 | cron-engine.md, agent-workspace.md, daemon.md | diff --git a/spec/DESIGN-v5.md b/spec/archive/v5/DESIGN-v5.md similarity index 100% rename from spec/DESIGN-v5.md rename to spec/archive/v5/DESIGN-v5.md diff --git a/spec/archive/v5/README.md b/spec/archive/v5/README.md new file mode 100644 index 0000000..5d855f0 --- /dev/null +++ b/spec/archive/v5/README.md @@ -0,0 +1,45 @@ +# Spec v5 Draft + +> **Date**: 2026-03-22 +> **Status**: Draft (reviewed) +> **구조**: 설계 개요 + 관심사별 상세 스펙 + 사용자 플로우 + +## 핵심 변경 (v4 → v5) + +- **Daemon = 상태 머신 + 실행기**: yaml에 정의된 prompt/script만 호출하는 단순 실행기로 축소 +- **Task trait 제거**: 5개 구현체 → prompt/script로 대체 +- **workspace = 1 repo**: 다른 DataSource 지원을 위한 추상화 +- **QueuePhase 8개**: Pending/Ready/Running/Completed/Done/HITL/Failed/Skipped +- **worktree는 인프라**: 항상 worktree에서 작업, Done 시 정리 +- **환경변수 최소화**: `WORK_ID` + `WORKTREE`만, 나머지는 `belt context` CLI +- **evaluate = CLI 도구 호출**: LLM이 `belt queue done/hitl` 실행 + +## 설계 문서 + +- **[DESIGN-v5.md](./DESIGN-v5.md)** — 설계 철학 + 전체 구조 개요 (간결) + +## 관심사별 상세 스펙 (concerns/) + +"이 시스템은 내부적으로 어떻게 동작하지?" — 구현자 대상 + +| 문서 | 설명 | +|------|------| +| [QueuePhase 상태 머신](./concerns/queue-state-machine.md) | 8개 phase 전이 다이어그램, worktree 생명주기, on_fail 조건 | +| [Daemon](./concerns/daemon.md) | 실행 루프 의사코드, concurrency, graceful shutdown | +| [DataSource](./concerns/datasource.md) | 외부 시스템 추상화 trait + context CLI + 워크플로우 yaml | +| [AgentRuntime](./concerns/agent-runtime.md) | LLM 실행 추상화 trait + Registry | +| [Agent 워크스페이스](./concerns/agent-workspace.md) | 대화형 에이전트 + evaluate + slash command | +| [Cron 엔진](./concerns/cron-engine.md) | 주기 실행 + 품질 루프 + force trigger | +| [CLI 레퍼런스](./concerns/cli-reference.md) | 3-layer SSOT + `belt context` + 전체 커맨드 트리 | + +## 사용자 플로우 (flows/) + +"사용자가 X를 하면 어떻게 되지?" — 시나리오 기반, 기획자/사용자 대상 + +| # | Flow | 설명 | +|---|------|------| +| 01 | [온보딩](./flows/01-setup.md) | workspace 등록 → 컨벤션 부트스트랩 | +| 02 | [스펙 생명주기](./flows/02-spec-lifecycle.md) | 스펙 등록 → 이슈 분해 → 완료 판정 | +| 03 | [이슈 파이프라인](./flows/03-issue-pipeline.md) | handlers 실행 → evaluate → on_done | +| 04 | [실패 복구와 HITL](./flows/04-failure-and-hitl.md) | escalation → on_fail → 사람 개입 | +| 05 | [모니터링](./flows/05-monitoring.md) | TUI + CLI + /agent 시각화 | diff --git a/spec/draft/concerns/agent-runtime.md b/spec/archive/v5/concerns/agent-runtime.md similarity index 100% rename from spec/draft/concerns/agent-runtime.md rename to spec/archive/v5/concerns/agent-runtime.md diff --git a/spec/draft/concerns/agent-workspace.md b/spec/archive/v5/concerns/agent-workspace.md similarity index 79% rename from spec/draft/concerns/agent-workspace.md rename to spec/archive/v5/concerns/agent-workspace.md index b0ecf7b..2437558 100644 --- a/spec/draft/concerns/agent-workspace.md +++ b/spec/archive/v5/concerns/agent-workspace.md @@ -10,30 +10,27 @@ 분류 로직은 코어에 속한다. Agent와 무관. -**v6 (#722)**: evaluate는 **per-work_id 단위**로 LLM 판정을 실행한다. - ``` handler 전부 성공 → Completed │ ▼ -Evaluator (Daemon tick에서 Executor보다 먼저 실행): - Progressive Pipeline: - Stage 1: Mechanical (cargo test 등, 비용 0) - → 실패 시 Retry (LLM 안 부름) - Stage 2: Semantic (LLM 1회, belt agent -p) - → LLM이 belt context로 컨텍스트 조회 후 판정 +evaluate cron (force_trigger로 즉시 실행 가능): + belt agent -p "Completed 아이템의 완료 여부를 판단해줘" + │ + │ LLM이 belt context로 컨텍스트 조회 후 CLI 도구로 결정: │ - ├── Done → hook.on_done() 트리거 - │ ├── hook 성공 → Done (worktree 정리) - │ └── hook 실패 → Failed (worktree 보존) + ├── belt queue done $WORK_ID + │ → Daemon이 on_done script 실행 (CLI 명령이 상태 전이를 트리거하고, Daemon이 script를 실행) + │ ├── script 성공 → Done (worktree 정리) + │ └── script 실패 → Failed (worktree 보존, 로그 기록) │ - └── HITL → HITL 이벤트 생성 → 사람 대기 (worktree 보존) + └── belt queue hitl $WORK_ID --reason "..." + → HITL 이벤트 생성 → 사람 대기 (worktree 보존) ``` -- Evaluator는 Daemon tick의 정규 단계. 상세: [Evaluator](./evaluator.md) -- SemanticStage에서 LLM이 `belt queue done/hitl` CLI를 직접 호출하여 상태를 전이한다 -- 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템 판정에 영향 없다 -- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비한다 +evaluate cron: `interval 60s + force_trigger on Completed 전이`. LLM이 JSON을 파싱하는 게 아니라, 직접 `belt queue done/hitl` CLI를 호출하여 상태를 전이한다. + +evaluate의 판단 입력: `belt context $WORK_ID --json` (queue 메타데이터 + 외부 시스템 컨텍스트 + append-only history). --- @@ -184,9 +181,14 @@ v4 (15개) → v5 (3개): ### classify-policy.md 로딩 경로 및 해석 (R-CW-007) `classify-policy.md`는 LLM 에이전트가 큐 아이템을 Done / HITL로 분류할 때 -참조하는 자연어 정책 문서다. `.claude/rules/` 하위에 위치하며, system prompt에 주입된다. +참조하는 정책 문서다. 두 가지 형태로 존재한다: -#### 로딩 경로 +| 파일 | 위치 | 소비자 | 용도 | +|------|------|--------|------| +| `classify-policy.md` | `.claude/rules/` 하위 | LLM agent (system prompt) | 자연어 분류 기준 | +| `classify-policy.yaml` | workspace root | daemon evaluator | machine-readable 라우팅 규칙 | + +#### 로딩 경로 (classify-policy.md) `agent::resolve_rules_dir` 함수가 아래 우선순위로 **디렉토리**를 탐색한다. 첫 번째로 존재하는 디렉토리 안의 **모든 `.md` 파일**이 로드된다. @@ -201,9 +203,11 @@ Priority 3: $BELT_HOME/claw-workspace/.claude/rules/ (global, belt claw init) #### 파일 미존재 시 fallback -- 디렉토리 자체가 없는 경우: agent는 built-in Claw rules(대화 턴 제한, 응답 포맷, 에러 핸들링)만으로 실행. 에러 없음. +- 디렉토리 자체가 없는 경우: agent는 built-in Claw rules(대화 턴 제한, 응답 포맷, + 에러 핸들링)만으로 실행. 에러 없음. - 디렉토리는 있지만 `.md` 파일이 없는 경우: 동일하게 built-in rules만 사용. -- `classify-policy.md`만 없고 다른 `.md`가 있는 경우: 다른 정책 파일은 정상 로드, 분류 정책 가이던스만 빠진 채 실행. +- `classify-policy.md`만 없고 다른 `.md`가 있는 경우: 다른 정책 파일은 정상 로드, + 분류 정책 가이던스만 빠진 채 실행. #### 구현 위치 @@ -223,22 +227,19 @@ Priority 3: $BELT_HOME/claw-workspace/.claude/rules/ (global, belt claw init) | `belt hitl list --json` | HITL 목록 조회 | 대화형 세션 | | `belt queue list --json` | 큐 목록 조회 | 대화형 세션 | -### Evaluator와의 관계 +### evaluate cron과의 관계 -Evaluator의 SemanticStage가 내부적으로 `belt agent -p`를 호출한다. 이때: -- **per-item**: 각 아이템에 대해 개별 프롬프트 발행 (v6 #722) -- LLM이 `belt context $WORK_ID`로 해당 아이템 정보를 조회 +evaluate cron은 내부적으로 `belt agent --workspace -p ""`를 호출한다. 이때: +- Completed 아이템 목록을 프롬프트에 포함 +- LLM이 `belt context`로 각 아이템 정보를 조회 - 판단 후 `belt queue done/hitl` CLI를 직접 호출하여 상태 전이 - classify-policy.md의 state별 Done 조건이 판단 기준 -- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비 - -상세: [Evaluator](./evaluator.md) --- ### 관련 문서 -- [DESIGN-v6](../DESIGN-v6.md) — QueuePhase 상태 머신 + evaluate 위치 +- [DESIGN-v5](../DESIGN-v5.md) — QueuePhase 상태 머신 + evaluate 위치 - [CLI 레퍼런스](./cli-reference.md) — CLI 전체 커맨드 트리 - [Cron 엔진](./cron-engine.md) — evaluate cron - [Data Model](./data-model.md) — 컨텍스트 모델 (belt context 출력) diff --git a/spec/draft/concerns/cli-reference.md b/spec/archive/v5/concerns/cli-reference.md similarity index 100% rename from spec/draft/concerns/cli-reference.md rename to spec/archive/v5/concerns/cli-reference.md diff --git a/spec/draft/concerns/cron-engine.md b/spec/archive/v5/concerns/cron-engine.md similarity index 62% rename from spec/draft/concerns/cron-engine.md rename to spec/archive/v5/concerns/cron-engine.md index 2c140b5..b1a26a8 100644 --- a/spec/draft/concerns/cron-engine.md +++ b/spec/archive/v5/concerns/cron-engine.md @@ -9,9 +9,7 @@ ``` 1. 인프라 유지 — hitl-timeout, log-cleanup, daily-report (결정적) -2. 품질 루프 — gap-detection, knowledge-extract (LLM 사용) - -※ evaluate는 Daemon tick 루프의 정규 단계로 이동. 상세: [Evaluator](./evaluator.md) +2. 품질 루프 — evaluate, gap-detection, knowledge-extract (LLM 사용) ``` --- @@ -59,6 +57,7 @@ gap 발견 → DataSource에서 open 아이템 조회 (Pending/Ready/Running) | Job | 주기 | 동작 | |-----|------|------| +| evaluate | 60초 | 완료 아이템 분류 (Done or HITL) — `belt agent -p` | | gap-detection | 1시간 | 스펙-코드 대조, gap 발견 시 이슈 생성 | | knowledge-extract | 1시간 | merged PR 지식 추출 | @@ -71,19 +70,39 @@ gap 발견 → DataSource에서 open 아이템 조회 (Pending/Ready/Running) --- -## Force Trigger +## Force Trigger (하이브리드 실행 모델) -force_trigger는 cron job을 다음 tick에서 우선 실행하도록 스케줄링한다. +evaluate는 **cron 주기 폴링 + force_trigger 즉시 실행**의 하이브리드 모델로 동작한다: ``` -force_trigger(job_name): - job.last_run_at = NULL → 다음 tick에서 즉시 실행 +1. 주기 폴링: evaluate cron이 60초마다 Completed 아이템을 스캔 +2. 즉시 실행: handler 성공 → Completed 전이 → force_trigger("evaluate") + → last_run_at = NULL → 다음 tick(10초 이내)에서 즉시 실행 ``` -- 동기적으로 실행하지 않는다. cron의 `last_run_at`을 리셋할 뿐. -- gap-detection 등 품질 루프 job에 사용. +- **force_trigger**는 동기적으로 evaluate를 실행하지 않는다. cron의 `last_run_at`을 리셋하여 다음 tick에서 우선 실행되도록 스케줄링한다. +- evaluate LLM 호출도 concurrency slot을 소비한다 (`daemon.max_concurrent`에 `active_evaluate_count`가 포함됨). +- 주기 폴링은 force_trigger가 누락된 경우(예: 프로세스 재시작)의 안전망 역할을 한다. + +> handler 실패 시에는 force_trigger 없이 즉시 escalation 정책이 적용된다 (evaluate 불필요). + +--- + +## 스크립트 구조 + +Cron job의 스크립트도 `belt context`를 활용하여 필요한 정보를 조회한다. -> **evaluate는 v6에서 Daemon tick 정규 단계로 이동**. Completed 아이템은 Evaluator가 다음 tick에서 Progressive Pipeline으로 판정한다. 상세: [Evaluator](./evaluator.md) +```bash +#!/bin/bash +# Guard: Completed 상태 아이템 있을 때만 +COMPLETED=$(belt queue list --workspace "$WORKSPACE" --phase completed --json | jq 'length') +if [ "$COMPLETED" = "0" ]; then exit 0; fi + +# 실행: evaluate +# LLM이 context를 조회하고 belt queue done/hitl CLI를 직접 호출 +belt agent --workspace "$WORKSPACE" -p \ + "Completed 아이템의 완료 여부를 판단하고, belt queue done 또는 belt queue hitl 을 실행해줘" +``` --- @@ -113,7 +132,6 @@ Cron 스크립트에는 workspace 정보가 필요하므로 추가 변수를 주 ### 관련 문서 -- [DESIGN-v6](../DESIGN-v6.md) — evaluate 아키텍처 +- [DESIGN-v5](../DESIGN-v5.md) — evaluate 아키텍처 - [DataSource](./datasource.md) — belt context 스키마 - [Agent](./agent-workspace.md) — evaluate와 Agent의 관계 -- [Stagnation Detection](./stagnation.md) — handler 실패 시 패턴 감지 (cron이 아닌 동기 실행) diff --git a/spec/draft/concerns/cross-platform.md b/spec/archive/v5/concerns/cross-platform.md similarity index 100% rename from spec/draft/concerns/cross-platform.md rename to spec/archive/v5/concerns/cross-platform.md diff --git a/spec/archive/v5/concerns/daemon.md b/spec/archive/v5/concerns/daemon.md new file mode 100644 index 0000000..bb11140 --- /dev/null +++ b/spec/archive/v5/concerns/daemon.md @@ -0,0 +1,186 @@ +# Daemon — 상태 머신 + 실행기 + +> Daemon은 yaml에 정의된 prompt/script를 호출하는 단순 실행기. +> GitHub 라벨, PR 생성 같은 도메인 로직을 모른다. + +--- + +## 역할 + +``` +1. 수집: DataSource.collect() → Pending에 넣기 +2. 전이: Pending → Ready → Running (자동, concurrency 제한) +3. 실행: yaml에 정의된 prompt/script 호출 +4. 완료: handler 성공 → Completed 전이 +5. 분류: evaluate cron이 Completed → Done or HITL 판정 (CLI 도구 호출) +6. 반영: on_done/on_fail script 실행 +7. 스케줄: Cron engine으로 주기 작업 실행 +``` + +--- + +## Concurrency 제어 + +두 레벨로 동시 실행을 제어한다: + +```yaml +# workspace.yaml — workspace 루트 레벨에 정의 +concurrency: 2 # 이 workspace에서 동시 Running 아이템 수 + +# daemon 글로벌 설정 (별도 config) — 전체 workspace 합산 상한 +max_concurrent: 4 +``` + +- **workspace.concurrency**: workspace yaml 루트에 정의. "이 프로젝트에 동시에 몇 개까지 돌릴까". 모든 source의 아이템 합산 기준. +- **daemon.max_concurrent**: "머신 리소스 한계" (evaluate cron의 LLM 호출도 slot을 소비) + +> **주의**: `concurrency`는 workspace 루트에 위치한다 (`sources.github` 하위가 아님). 하나의 workspace에 여러 source가 있을 수 있으므로, per-source가 아닌 per-workspace 기준으로 제어한다. + +Daemon은 `Ready → Running` 전이 시 두 제한을 모두 확인한다. + +--- + +## 실행 루프 (의사코드) + +``` +loop { + // 1. 수집 + for source in workspace.sources: + items = source.collect() + queue.push(Pending, items) + + // 2. 자동 전이 + 실행 (2단계 concurrency 제한) + queue.advance_all(Pending → Ready) + ws_slots = workspace.concurrency - queue.count(Running, workspace) + global_slots = daemon.max_concurrent - queue.count_all(Running) - active_evaluate_count + limit = min(ws_slots, global_slots) + queue.advance(Ready → Running, limit=limit) + + for item in queue.get_new(Running): + state = lookup_state(item) + + // worktree 생성 (인프라) + worktree = create_or_reuse_worktree(item) + + // on_enter (실패 시 handler를 실행하지 않고 escalation 적용) + result = run_actions(state.on_enter, WORK_ID=item.id, WORKTREE=worktree) + if result.failed: + failure_count = count_failures(item.source_id, item.state) + escalation = lookup_escalation(failure_count) + if escalation != retry: + run_actions(state.on_fail, WORK_ID=item.id, WORKTREE=worktree) + apply_escalation(item, escalation) + continue + + // handlers 순차 실행 + for action in state.handlers: + result = execute(action, WORK_ID=item.id, WORKTREE=worktree) + if result.failed: + failure_count = count_failures(item.source_id, item.state) // history에서 계산 + escalation = lookup_escalation(failure_count) + if escalation != retry: + run_actions(state.on_fail, WORK_ID=item.id, WORKTREE=worktree) + apply_escalation(item, escalation) // retry: worktree 보존 + break + else: + // 모든 handler 성공 → Completed + queue.transit(item, Completed) + force_trigger("evaluate") + + // 3. cron tick (evaluate, gap-detection 등) + cron_engine.tick() +} + +// evaluate cron (force_trigger 가능): +// LLM이 직접 CLI를 호출하여 상태 전이 (JSON 파싱 불필요) +for item in queue.get(Completed): + belt_agent_p(workspace, "Completed 아이템 $WORK_ID 의 완료 여부를 판단하고, + belt queue done $WORK_ID 또는 belt queue hitl $WORK_ID 를 실행해줘") + // → LLM이 context를 조회하고 판단 후 CLI 실행 + // belt queue done $WORK_ID → on_done script 실행 → Done (worktree 정리) + // └── script 실패 → Failed (worktree 보존) + // belt queue hitl $WORK_ID → HITL 이벤트 생성 (worktree 보존) +``` + +--- + +## 통합 액션 타입 + +두 가지 실행 단위가 있으며, 사용 위치에 따라 허용 범위가 다르다: + +```yaml +- prompt: "..." # → AgentRuntime.invoke() (LLM, worktree 안에서) +- script: "..." # → bash 실행 (결정적, WORK_ID + WORKTREE 주입) +``` + +| 위치 | prompt | script | 설명 | +|------|:------:|:------:|------| +| `handlers` | O | O | 워크플로우 핵심 작업 | +| `on_enter` | X | O | Running 진입 시 사전 작업 | +| `on_done` | X | O | 완료 후 외부 시스템 반영 | +| `on_fail` | X | O | 실패 시 외부 시스템 알림 | + +lifecycle hook(`on_enter`/`on_done`/`on_fail`)은 **script만 허용**한다. 결정적 실행이 보장되어야 하고, LLM 호출은 handler에서만 수행한다. 상세는 [Data Model](./data-model.md#액션-타입) 참조. + +script 안에서 `belt context $WORK_ID --json`을 호출하여 필요한 정보를 조회한다. + +--- + +## 환경변수 + +Daemon이 prompt/script 실행 시 주입하는 환경변수는 **2개만**: + +| 변수 | 설명 | +|------|------| +| `WORK_ID` | 큐 아이템 식별자 | +| `WORKTREE` | worktree 경로 | + +나머지는 `belt context $WORK_ID --json`으로 조회. 상세는 [DataSource](./datasource.md) 참조. + +--- + +## Graceful Shutdown + +``` +SIGINT → on_shutdown: + 1. Running 아이템 완료 대기 (timeout: 30초) + → timeout 초과: Pending으로 롤백, worktree 보존 + (재시작 후 해당 아이템이 다시 Ready → Running 전이 시 기존 worktree를 재사용) + 2. Cron engine 정지 +``` + +> **worktree 보존 원칙**: shutdown 롤백 시 worktree를 정리하지 않는다. retry와 동일하게, 재시작 후 이전 작업 위에서 이어서 진행할 수 있도록 worktree를 보존한다. 좀비 worktree는 `log-cleanup` cron이 TTL 기반으로 정리한다. + +--- + +--- + +## 수용 기준 + +### Concurrency 제어 + +- [ ] workspace.concurrency=2인 workspace에서 Running 아이템이 2개이면 추가 Ready→Running 전이가 발생하지 않는다 +- [ ] daemon.max_concurrent에 도달하면 모든 workspace에서 Ready→Running 전이가 중단된다 +- [ ] evaluate LLM 호출도 concurrency slot을 소비한다 + +### Graceful Shutdown + +- [ ] SIGINT 수신 시 새 아이템 수집/전이를 즉시 중단한다 +- [ ] Running 아이템의 완료를 최대 30초 대기한다 +- [ ] 30초 초과 시 Running 아이템을 Pending으로 롤백하고 worktree를 보존한다 +- [ ] 재시작 후 롤백된 아이템이 기존 worktree를 재사용하여 Running에 재진입한다 + +### 환경변수 + +- [ ] handler/script 실행 시 WORK_ID, WORKTREE 두 환경변수만 주입된다 +- [ ] `belt context $WORK_ID --json`이 아이템의 전체 정보를 반환한다 + +--- + +### 관련 문서 + +- [DESIGN-v5](../DESIGN-v5.md) — 설계 철학 +- [QueuePhase 상태 머신](./queue-state-machine.md) — 상태 전이 상세 +- [DataSource](./datasource.md) — 워크플로우 정의 + context 스키마 +- [AgentRuntime](./agent-runtime.md) — LLM 실행 추상화 +- [Cron 엔진](./cron-engine.md) — evaluate cron + 품질 루프 diff --git a/spec/draft/concerns/data-model.md b/spec/archive/v5/concerns/data-model.md similarity index 66% rename from spec/draft/concerns/data-model.md rename to spec/archive/v5/concerns/data-model.md index eb34c1d..2508675 100644 --- a/spec/draft/concerns/data-model.md +++ b/spec/archive/v5/concerns/data-model.md @@ -1,6 +1,6 @@ # Data Model -> 관련 문서: [DESIGN-v6](../DESIGN-v6.md), [QueuePhase 상태 머신](./queue-state-machine.md), [DataSource](./datasource.md), [LifecycleHook](./lifecycle-hook.md), [Evaluator](./evaluator.md), [Cron 엔진](./cron-engine.md), [Stagnation](./stagnation.md) +> 관련 문서: [DESIGN-v5](../DESIGN-v5.md), [QueuePhase 상태 머신](./queue-state-machine.md), [DataSource](./datasource.md), [Cron 엔진](./cron-engine.md) Belt의 모든 상태는 SQLite 단일 파일(`~/.belt/belt.db`)에 저장된다. 이 문서는 테이블 스키마, 도메인 모델, 직렬화 규칙을 한 곳에 정의한다. @@ -46,7 +46,7 @@ CREATE TABLE queue_items ( hitl_notes TEXT, -- 사용자 메모 hitl_reason TEXT, -- HitlReason enum (snake_case) hitl_timeout_at TEXT, -- 만료 시각 (RFC3339) - hitl_terminal_action TEXT, -- EscalationAction enum (snake_case) ← v6: Option → Option + hitl_terminal_action TEXT, -- 만료 시 액션 ('skip' | 'replan') -- 추적 필드 replan_count INTEGER NOT NULL DEFAULT 0, -- 재계획 횟수 (max 3) @@ -54,14 +54,12 @@ CREATE TABLE queue_items ( ); ``` -**v6 변경 (#720)**: `hitl_terminal_action`은 `EscalationAction` enum 값만 허용한다. DB에는 snake_case 문자열로 저장, 로드 시 `FromStr`로 파싱한다. 유효하지 않은 값은 파싱 에러. - **인메모리 전용 필드** (DB에 저장하지 않음): - `previous_worktree_path: Option` — retry 시 이전 아이템의 worktree 경로를 전달하기 위한 transient 필드 ### history -작업 시도 기록. append-only로만 쓰고, 읽기 전용으로 조회한다. `failure_count`는 이 테이블에서 계산한다. **Stagnation detection도 이 테이블의 summary/error를 입력으로 사용한다.** +작업 시도 기록. append-only로만 쓰고, 읽기 전용으로 조회한다. `failure_count`는 이 테이블에서 계산한다. ```sql CREATE TABLE history ( @@ -77,23 +75,9 @@ CREATE TABLE history ( ); ``` -**QueuePhase ↔ history.status 매핑**: - -| QueuePhase | history.status | 비고 | -|------------|---------------|------| -| Pending | — | 시도 아님, 기록 안 함 | -| Ready | — | 시도 아님, 기록 안 함 | -| Running | `running` | handler 실행 중 | -| Completed | — | 전이 상태, history에 기록 안 함 (`"completed"` 파싱 시 `Done`으로 매핑) | -| Done | `done` | 완료 | -| Hitl | `hitl` | 사람 대기 | -| Failed | `failed` | 실패 | -| Skipped | `skipped` | 건너뜀 | - **파생 쿼리**: - `failure_count`: `SELECT COUNT(*) FROM history WHERE source_id = ? AND state = ? AND status = 'failed'` - `max_attempt`: `SELECT MAX(attempt) FROM history WHERE source_id = ? AND state = ?` -- **stagnation 입력**: `SELECT summary, error FROM history WHERE source_id = ? AND state = ? ORDER BY attempt DESC LIMIT ?` ### transition_events @@ -104,7 +88,7 @@ CREATE TABLE transition_events ( id TEXT PRIMARY KEY, -- UUID work_id TEXT NOT NULL, source_id TEXT NOT NULL, - event_type TEXT NOT NULL, -- 'phase_enter' | 'handler' | 'evaluate' | 'hook' | 'stagnation' + event_type TEXT NOT NULL, -- 'phase_enter' | 'handler' | 'evaluate' | 'on_done' | 'on_fail' phase TEXT, -- 진입한 phase from_phase TEXT, -- 이전 phase detail TEXT, -- 사람이 읽을 수 있는 설명 @@ -112,14 +96,10 @@ CREATE TABLE transition_events ( ); ``` -**v6 변경**: `event_type`에 `'stagnation'`과 `'hook'` 추가. 기존 `'on_done'`/`'on_fail'`은 `'hook'`으로 통합 — LifecycleHook 트리거 이벤트를 기록한다. `detail`에 hook 종류(on_enter/on_done/on_fail/on_escalation), 탐지 패턴, confidence, evidence를 JSON으로 기록한다. - ### queue_dependencies 아이템 간 실행 순서 제약. `depends_on` 아이템이 Done이 아니면 `work_id` 아이템은 Ready→Running 전이가 블로킹된다. -**v6 변경 (#721)**: dependency phase 확인은 **DB 조회 기반**이다 (in-memory queue가 아님). 재시작 후에도 정확히 동작한다. - ```sql CREATE TABLE queue_dependencies ( work_id TEXT NOT NULL, @@ -244,13 +224,11 @@ CREATE TABLE knowledge_base ( | `Ready` | `"ready"` | No | 실행 준비 완료 (자동 전이) | | `Running` | `"running"` | No | worktree 생성 + handler 실행 중 | | `Completed` | `"completed"` | No | handler 성공, evaluate 대기 | -| `Done` | `"done"` | **Yes** | evaluate 완료 + hook.on_done() 성공 | +| `Done` | `"done"` | **Yes** | evaluate 완료 + on_done 성공 | | `Hitl` | `"hitl"` | No | 사람 판단 필요 | -| `Failed` | `"failed"` | No | hook.on_done() 실패 또는 인프라 오류 | +| `Failed` | `"failed"` | No | on_done 실패 또는 인프라 오류 | | `Skipped` | `"skipped"` | **Yes** | escalation skip 또는 preflight 실패 | -**v6 (#718)**: `phase` 필드는 `pub(crate)` 가시성. 외부에서 직접 대입 불가, 반드시 `QueueItem::transit()` 경유. 상세: [QueuePhase 상태 머신](./queue-state-machine.md) - ### SpecStatus 6개 상태. 직렬화 시 lowercase. @@ -277,57 +255,18 @@ HITL 생성 경로. 직렬화 시 snake_case. | `SpecConflict` | `"spec_conflict"` | 스펙 파일 겹침 | | `SpecCompletionReview` | `"spec_completion_review"` | 스펙 완료 최종 확인 | | `SpecModificationProposed` | `"spec_modification_proposed"` | Agent 수정 제안 | -| `StagnationDetected` | `"stagnation_detected"` | **v6** 반복 패턴 감지 + lateral thinking 사고 전환 | ### EscalationAction failure_count별 대응. 직렬화 시 snake_case. -| Variant | 직렬화 | hook.on_fail 트리거 | 설명 | -|---------|--------|:------------------:|------| +| Variant | 직렬화 | on_fail 실행 | 설명 | +|---------|--------|:------------:|------| | `Retry` | `"retry"` | **No** | 조용한 재시도 | -| `RetryWithComment` | `"retry_with_comment"` | Yes | hook.on_fail + 재시도 | -| `Hitl` | `"hitl"` | Yes | hook.on_fail + HITL 생성 | -| `Skip` | `"skip"` | Yes | hook.on_fail + Skipped | -| `Replan` | `"replan"` | Yes | hook.on_fail + HITL(replan) | - -모든 EscalationAction에서 `hook.on_escalation(action)` 트리거. `on_fail`은 Retry 제외 시 추가 트리거. - -**v6 (#720)**: `EscalationAction`은 `FromStr` + `Display` impl을 가진다. `hitl_terminal_action` 필드 타입으로도 사용된다. - -```rust -impl FromStr for EscalationAction { - type Err = BeltError; - fn from_str(s: &str) -> Result { /* snake_case 파싱 */ } -} -``` - -### StagnationPattern (v6 신규) - -정체 패턴 유형. 직렬화 시 snake_case. - -| Variant | 직렬화 | 설명 | -|---------|--------|------| -| `Spinning` | `"spinning"` | 동일/유사 출력 반복 (A→A→A) | -| `Oscillation` | `"oscillation"` | 교대 반복 (A→B→A→B) | -| `NoDrift` | `"no_drift"` | 진행 점수 정체 | -| `DiminishingReturns` | `"diminishing_returns"` | 개선폭 감소 | - -SPINNING/OSCILLATION은 `CompositeSimilarity`로 유사도 판단, NO_DRIFT/DIMINISHING은 drift score 수치 비교. - -### Persona (v6 신규) - -Lateral Thinking 사고 전환 페르소나. belt-core에 `include_str!`로 내장. 직렬화 시 snake_case. - -| Variant | 직렬화 | 패턴 친화도 | 전략 | -|---------|--------|-----------|------| -| `Hacker` | `"hacker"` | SPINNING | 제약 우회, 워크어라운드 | -| `Architect` | `"architect"` | OSCILLATION | 구조 재설계, 관점 전환 | -| `Researcher` | `"researcher"` | NO_DRIFT | 정보 수집, 체계적 디버깅 | -| `Simplifier` | `"simplifier"` | DIMINISHING | 복잡도 축소, 가정 제거 | -| `Contrarian` | `"contrarian"` | 복합/기타 | 가정 뒤집기, 문제 역전 | - -상세: [Stagnation Detection](./stagnation.md) +| `RetryWithComment` | `"retry_with_comment"` | Yes | on_fail + 재시도 | +| `Hitl` | `"hitl"` | Yes | on_fail + HITL 생성 | +| `Skip` | `"skip"` | Yes | on_fail + Skipped | +| `Replan` | `"replan"` | Yes | on_fail + HITL(replan) | ### HistoryStatus @@ -359,23 +298,20 @@ handlers: - script: "cargo test" ``` -### ScriptAction (yaml 설정, v6 호환용) +### ScriptAction (yaml 설정) -v6 Phase 1에서 기존 yaml과의 호환을 위해 유지. `ScriptLifecycleHook` 어댑터가 이를 소비한다. Phase 2에서 DataSource별 Hook impl로 대체 예정. +lifecycle hook(`on_enter`, `on_done`, `on_fail`)에서 사용. **script만 허용**. ```yaml -# v6 Phase 1: 기존 yaml 호환 (ScriptLifecycleHook 어댑터가 처리) on_done: - script: "gh pr create ..." on_fail: - script: "gh issue comment ..." ``` -> **v6 변경**: lifecycle 반응은 `LifecycleHook` trait으로 분리. yaml script는 어댑터를 통해 호환 유지. 상세: [LifecycleHook](./lifecycle-hook.md) - ### Action (런타임 추상화) -코어의 실행 단위. handler의 `HandlerConfig`가 `Action`으로 변환되어 Executor가 실행한다. +코어의 실행 단위. `HandlerConfig`와 `ScriptAction` 모두 `Action`으로 변환되어 실행된다. ``` HandlerConfig::Prompt → Action::Prompt { text, runtime, model } @@ -411,29 +347,6 @@ sources: 3: hitl terminal: skip # HITL 만료 시 ('skip' | 'replan') -# v6 신규: stagnation 탐지 + lateral thinking 설정 -stagnation: - enabled: true # default: true - spinning_threshold: 3 # default: 3 - oscillation_cycles: 2 # default: 2 - similarity_threshold: 0.8 # composite score 유사 판정 (default: 0.8) - no_drift_epsilon: 0.01 # default: 0.01 - no_drift_iterations: 3 # default: 3 - diminishing_threshold: 0.01 # default: 0.01 - confidence_threshold: 0.5 # default: 0.5 - - similarity: # CompositeSimilarity (Composite Pattern) - - judge: exact_hash # 기본 프리셋 - weight: 0.5 - - judge: token_fingerprint - weight: 0.3 - - judge: ncd - weight: 0.2 - - lateral: - enabled: true # default: true - max_attempts: 3 # 페르소나 최대 시도 (default: 3) - runtime: default: claude claude: @@ -455,8 +368,6 @@ runtime: `belt context $WORK_ID --json`이 반환하는 구조. script가 정보를 조회하는 유일한 방법. -**v6 변경 (#719)**: `source_data` 필드 추가. DataSource별 자유 스키마. 기존 `issue`/`pr` 필드는 호환성을 위해 유지. - ```json { "work_id": "github:org/repo#42:implement", @@ -471,27 +382,6 @@ runtime: "url": "https://github.com/org/repo", "default_branch": "main" }, - "source_data": { - "issue": { - "number": 42, - "title": "...", - "body": "...", - "labels": ["belt:implement"], - "author": "user", - "state": "open" - }, - "pr": { - "number": 43, - "title": "...", - "state": "open", - "draft": false, - "head_branch": "belt/42-implement", - "base_branch": "main", - "reviews": [ - { "reviewer": "user", "state": "APPROVED" } - ] - } - }, "issue": { "number": 42, "title": "...", @@ -524,27 +414,6 @@ runtime: } ``` -### source_data 마이그레이션 전략 (#719) - -| Phase | 상태 | 설명 | -|-------|------|------| -| **1 (v6)** | `source_data` + `issue`/`pr` 양쪽 채움 | 하위 호환. 기존 script 수정 불필요 | -| **2 (v7+)** | `issue`/`pr` deprecated | script가 `source_data` 경로로 전환 | -| **3 (v8+)** | `issue`/`pr` 제거 | `source_data`만 사용 | - -script에서의 접근: -```bash -# v6 (양쪽 모두 가능) -belt context $WORK_ID --json | jq '.issue.number' -belt context $WORK_ID --json | jq '.source_data.issue.number' - -# v7+ (source_data 권장) -belt context $WORK_ID --json | jq '.source_data.issue.number' - -# Jira DataSource (v7+) -belt context $WORK_ID --json | jq '.source_data.ticket.key' -``` - --- ## 타임스탬프 규칙 diff --git a/spec/draft/concerns/datasource.md b/spec/archive/v5/concerns/datasource.md similarity index 66% rename from spec/draft/concerns/datasource.md rename to spec/archive/v5/concerns/datasource.md index c05d0e8..38b7cbc 100644 --- a/spec/draft/concerns/datasource.md +++ b/spec/archive/v5/concerns/datasource.md @@ -8,19 +8,17 @@ ## 역할 ``` -DataSource가 소유하는 것 (읽기): - 1. 수집 — 어떤 조건에서 아이템을 감지하는가 (collect) - 2. 컨텍스트 — 해당 아이템의 외부 시스템 정보를 어떻게 조회하는가 (get_context) +DataSource가 소유하는 것: + 1. 수집 — 어떤 조건에서 아이템을 감지하는가 (trigger) + 2. 컨텍스트 — 해당 아이템의 외부 시스템 정보를 어떻게 조회하는가 (context) -LifecycleHook이 소유하는 것 (쓰기/반응): - 3. 상태 반응 — 상태 전이 시 외부 시스템에 어떻게 반영하는가 (on_enter/on_done/on_fail/on_escalation) +코어/yaml이 소유하는 것: + 3. 처리 — 감지된 아이템을 어떻게 처리하는가 (handlers: prompt/script) + 4. 전이 — 처리 완료 후 다음에 뭘 트리거하는가 (on_done script) + 5. 실패 반영 — 실패 시 외부 시스템에 어떻게 알리는가 (on_fail script) + 6. 실패 정책 — 실패 시 어떻게 escalation하는가 (escalation) -yaml이 소유하는 것: - 4. 처리 — 감지된 아이템을 어떻게 처리하는가 (handlers: prompt/script) - 5. 실패 정책 — 실패 시 어떻게 escalation하는가 (escalation) - -코어는 DataSource/LifecycleHook 내부를 모른다. collect() 결과를 큐에 넣고, 상태 전이 시 hook을 트리거할 뿐. -상세: [LifecycleHook](./lifecycle-hook.md) +코어는 DataSource 내부를 모른다. collect() 결과를 큐에 넣고, 상태 전이만 관리. ``` --- @@ -42,11 +40,9 @@ pub trait DataSource: Send + Sync { ``` v4 대비 대폭 축소. `on_phase_enter`, `on_failed`, `on_done`, `before_task`, `after_task` 모두 제거. -- on_done/on_fail/on_enter/on_escalation → `LifecycleHook` trait으로 분리. 상세: [LifecycleHook](./lifecycle-hook.md) +- on_done/on_fail → yaml에 정의된 script가 처리 (gh CLI 등 직접 호출) - worktree 셋업 → 인프라 레이어가 항상 처리 -- escalation → yaml의 escalation 정책을 코어가 결정, hook이 반응 - -**v6 (#719)**: `get_context()`가 반환하는 `ItemContext`에 `source_data: serde_json::Value` 필드가 추가된다. DataSource는 자신의 고유 데이터를 `source_data`에 자유 스키마로 채운다. +- escalation → yaml의 escalation 정책을 코어가 실행 --- @@ -71,8 +67,6 @@ Daemon이 주입하는 환경변수는 **2개만**: ### GitHub context 스키마 -**v6 (#719)**: `source_data` 필드에 DataSource 고유 데이터를 담는다. 기존 `issue`/`pr` 필드는 Phase 1에서 호환성을 위해 유지. - ```json { "work_id": "github:org/repo#42:implement", @@ -87,20 +81,6 @@ Daemon이 주입하는 환경변수는 **2개만**: "url": "https://github.com/org/repo", "default_branch": "main" }, - "source_data": { - "issue": { - "number": 42, - "title": "JWT middleware 구현", - "body": "...", - "labels": ["belt:implement"], - "author": "irene" - }, - "pr": { - "number": 87, - "head_branch": "feat/jwt-middleware", - "review_comments": [] - } - }, "issue": { "number": 42, "title": "JWT middleware 구현", @@ -111,7 +91,7 @@ Daemon이 주입하는 환경변수는 **2개만**: "pr": { "number": 87, "head_branch": "feat/jwt-middleware", - "review_comments": [] + "review_comments": [...] }, "history": [ { "state": "analyze", "status": "done", "attempt": 1, "summary": "구현 가능" }, @@ -122,8 +102,6 @@ Daemon이 주입하는 환경변수는 **2개만**: } ``` -> **source_data 마이그레이션**: Phase 1(v6)에서는 `issue`/`pr`과 `source_data` 양쪽 모두 채운다. Phase 2(v7+)에서 기존 필드 deprecated, Phase 3(v8+)에서 제거. 상세: [Data Model](./data-model.md#source_data-마이그레이션-전략-719) - ### history는 append-only 같은 `source_id`의 모든 이벤트가 시간순으로 축적된다. 실패 횟수는 history에서 계산: @@ -135,7 +113,7 @@ FAILURES=$(echo $CTX | jq '[.history[] | select(.status=="failed" and .state=="i 별도 `failure_count` 컬럼 없이 history 조회만으로 충분. -### Jira context 스키마 (v7+) +### Jira context 스키마 (v6+) ```json { @@ -150,25 +128,21 @@ FAILURES=$(echo $CTX | jq '[.history[] | select(.status=="failed" and .state=="i "type": "jira", "url": "https://jira.company.com/project/BE" }, - "source_data": { - "ticket": { - "key": "BE-123", - "summary": "...", - "status": "In Progress", - "assignee": "irene" - } + "ticket": { + "key": "BE-123", + "summary": "...", + "status": "In Progress", + "assignee": "irene" }, - "history": [] + "history": [...] } ``` -> Jira DataSource는 `source_data.ticket`에 데이터를 채운다. `issue`/`pr` 필드는 없음 — `source_data`만으로 OCP 달성. - --- ## 상태 기반 워크플로우 -각 DataSource는 자기 시스템의 상태 표현으로 워크플로우를 정의한다. v6는 GitHub에 집중한다. +각 DataSource는 자기 시스템의 상태 표현으로 워크플로우를 정의한다. v5는 GitHub에 집중한다. ### GitHub (라벨 기반) @@ -186,7 +160,7 @@ sources: on_done: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') + ISSUE=$(echo $CTX | jq -r '.issue.number') REPO=$(echo $CTX | jq -r '.source.url') gh issue edit $ISSUE --remove-label "belt:analyze" -R $REPO gh issue edit $ISSUE --add-label "belt:implement" -R $REPO @@ -198,9 +172,9 @@ sources: on_done: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') + ISSUE=$(echo $CTX | jq -r '.issue.number') REPO=$(echo $CTX | jq -r '.source.url') - TITLE=$(echo $CTX | jq -r '.source_data.issue.title // .issue.title') + TITLE=$(echo $CTX | jq -r '.issue.title') gh pr create --title "$TITLE" --body "Closes #$ISSUE" -R $REPO gh issue edit $ISSUE --remove-label "belt:implement" -R $REPO gh issue edit $ISSUE --add-label "belt:review" -R $REPO @@ -212,7 +186,7 @@ sources: on_done: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') + ISSUE=$(echo $CTX | jq -r '.issue.number') REPO=$(echo $CTX | jq -r '.source.url') gh issue edit $ISSUE --remove-label "belt:review" -R $REPO gh issue edit $ISSUE --add-label "belt:done" -R $REPO @@ -224,15 +198,15 @@ sources: terminal: skip # hitl timeout 시 적용 (skip 또는 replan) ``` -### 향후 확장 (v7+) +### 향후 확장 (v6+) -DataSource trait을 구현하면 코어 변경 없이 새 외부 시스템을 추가할 수 있다. `source_data`를 통해 코어 타입 변경도 불필요. +DataSource trait을 구현하면 코어 변경 없이 새 외부 시스템을 추가할 수 있다. -| 시스템 | 상태 표현 | trigger 예시 | source_data | -|--------|----------|-------------|-------------| -| Jira | 티켓 status | `{ status: "To Analyze" }` | `source_data.ticket` | -| Slack | 리액션 | `{ reaction: "robot_face" }` | `source_data.message` | -| Linear | 라벨/status | `{ label: "belt" }` | `source_data.issue` | +| 시스템 | 상태 표현 | trigger 예시 | +|--------|----------|-------------| +| Jira | 티켓 status | `{ status: "To Analyze" }` | +| Slack | 리액션 | `{ reaction: "robot_face" }` | +| Linear | 라벨/status | `{ label: "belt" }` | --- @@ -253,18 +227,36 @@ handler 배열은 Running 상태에서 순차 실행. 하나라도 실패 시 on --- -## Lifecycle Hook — LifecycleHook trait으로 분리 +## on_done / on_fail / on_enter -v6에서 on_done/on_fail/on_enter/on_escalation은 `LifecycleHook` trait으로 분리된다. Daemon은 상태 전이 시 hook을 트리거만 하고, 실행 책임은 Hook impl이 가진다. +모든 lifecycle hook은 **script 배열**로 정의. handler와 동일한 통합 액션 타입. -상세: [LifecycleHook](./lifecycle-hook.md) +```yaml +states: + implement: + trigger: { label: "belt:implement" } + on_enter: # Running 진입 시 (선택) + - script: | + CTX=$(belt context $WORK_ID --json) + echo "시작: $(echo $CTX | jq -r '.issue.title')" + handlers: + - prompt: "이슈를 구현해줘" + on_done: # 성공 시 + - script: | + CTX=$(belt context $WORK_ID --json) + # PR 생성, 라벨 전환 등 + on_fail: # 실패 시 (escalation 전) + - script: | + CTX=$(belt context $WORK_ID --json) + ISSUE=$(echo $CTX | jq -r '.issue.number') + REPO=$(echo $CTX | jq -r '.source.url') + gh issue comment $ISSUE --body "구현 실패" -R $REPO +``` -| hook | 트리거 시점 | 실패 시 | -|------|-----------|--------| -| `on_enter` | Running 진입 후, handler 실행 전 | handler 건너뛰고 escalation | -| `on_done` | evaluate가 Done 판정 후 | Failed 상태로 전이 | -| `on_fail` | handler/on_enter 실패 시 (retry 제외) | — | -| `on_escalation` | escalation 결정 후 | — | +실행 주체: Daemon이 상태 전이 시점에 직접 실행. +- `on_enter`: Running 진입 후, handler 실행 전. **실패 시 handler를 실행하지 않고 즉시 escalation 정책을 적용**한다 (handler 실패와 동일한 경로). on_enter 실패도 history에 failed 이벤트로 기록되며 failure_count에 포함된다. +- `on_done`: evaluate가 Done 판정 후 (script 실패 시 → Failed 상태) +- `on_fail`: handler 또는 on_enter 실패 시, escalation level에 따라 조건부 실행 (`retry`에서는 실행 안 함) --- @@ -286,7 +278,12 @@ escalation: # 선택지: skip (종료) 또는 replan (스펙 수정 제안) ``` -> **v6 Stagnation과 Escalation의 관계**: escalation은 failure_count 기반으로 결정되고, stagnation은 lateral_plan 주입에 집중한다. 두 관심사는 직교한다 — escalation이 "언제 멈출지"를 결정하고, stagnation이 "다르게 시도할지"를 결정한다. escalation 발생 시 `LifecycleHook.on_escalation()`이 DataSource별 반응을 처리한다. 상세: [LifecycleHook](./lifecycle-hook.md) +> **설계 의도**: level 4(skip)와 level 5(replan)는 순차적으로 도달할 수 없다. skip은 terminal 상태(Skipped)이므로 이후 실패가 발생하지 않는다. 따라서 skip과 replan은 level 3(hitl) 이후의 **대안적 선택지**로 재설계하였다. +> +> - `terminal: skip` — hitl timeout 시 해당 아이템을 건너뛰고 종료 +> - `terminal: replan` — hitl timeout 시 스펙 수정을 제안하는 HITL(replan) 이벤트 생성 +> +> 사람이 hitl에 직접 응답하는 경우, done/retry/skip/replan 중 자유롭게 선택할 수 있다. ### on_fail 실행 조건 @@ -328,10 +325,7 @@ queue_items 테이블: ### 관련 문서 -- [DESIGN-v6](../DESIGN-v6.md) — 전체 아키텍처 -- [LifecycleHook](./lifecycle-hook.md) — 상태 전이 반응 trait +- [DESIGN-v5](../DESIGN-v5.md) — 전체 아키텍처 - [AgentRuntime](./agent-runtime.md) — handler prompt 실행 -- [Stagnation Detection](./stagnation.md) — 실패 패턴 감지 - [Cron 엔진](./cron-engine.md) — 품질 루프 - [CLI 레퍼런스](./cli-reference.md) — belt context CLI -- [Data Model](./data-model.md) — source_data 마이그레이션 전략 diff --git a/spec/draft/concerns/distribution.md b/spec/archive/v5/concerns/distribution.md similarity index 100% rename from spec/draft/concerns/distribution.md rename to spec/archive/v5/concerns/distribution.md diff --git a/spec/draft/concerns/queue-state-machine.md b/spec/archive/v5/concerns/queue-state-machine.md similarity index 68% rename from spec/draft/concerns/queue-state-machine.md rename to spec/archive/v5/concerns/queue-state-machine.md index e1ddade..2587622 100644 --- a/spec/draft/concerns/queue-state-machine.md +++ b/spec/archive/v5/concerns/queue-state-machine.md @@ -1,7 +1,7 @@ # QueuePhase 상태 머신 > 큐 아이템의 전체 생명주기를 정의한다. -> 상위 설계는 [DESIGN-v6](../DESIGN-v6.md) 참조. +> 상위 설계는 [DESIGN-v5](../DESIGN-v5.md) 참조. --- @@ -20,46 +20,6 @@ --- -## Phase 전이 캡슐화 (v6 #718) - -`QueueItem.phase` 필드를 직접 대입하면 `can_transition_to()` 검증을 우회할 수 있다. -v6에서는 모든 전이를 `QueueItem::transit()` 메서드로 강제한다. - -```rust -impl QueueItem { - /// phase 필드는 pub(crate) — belt-core 외부에서 직접 대입 불가 - /// 읽기는 pub getter: fn phase(&self) -> QueuePhase - - pub fn transit(&mut self, to: QueuePhase) -> Result { - let from = self.phase; - if !from.can_transition_to(&to) { - return Err(BeltError::InvalidTransition { from, to }); - } - self.phase = to; - self.updated_at = Utc::now().to_rfc3339(); - Ok(from) - } -} -``` - -### 테스트 지원 - -테스트에서 특정 phase의 아이템을 생성하려면 빌더를 사용한다: - -```rust -// 테스트 전용 빌더 (cfg(test) 또는 #[doc(hidden)]) -QueueItem::builder() - .work_id("test:1:analyze") - .with_phase(QueuePhase::Running) // 검증 없이 직접 설정 - .build() -``` - -### DB 로드 - -`belt-infra/db.rs`의 `from_row()`는 `pub(crate)` 접근 가능하므로 DB에서 로드 시 phase 직접 설정이 가능하다. - ---- - ## 전체 상태 전이 ``` @@ -92,30 +52,20 @@ QueueItem::builder() │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────────────────┐ - │ Completed │ │ Stagnation Analyzer (항상 실행)│ - │ │ │ │ - │ handler 완료 │ │ ① CompositeSimilarity로 │ - │ evaluate 대기 │ │ outputs/errors 유사도 분석 │ - │ │ │ ② 패턴 감지 시 │ - │ force_trigger │ │ LateralAnalyzer가 │ - │ ("evaluate") │ │ 내장 페르소나로 대안 분석 │ - └────────┬────────┘ │ → lateral_plan 생성 │ - │ │ │ - │ │ Escalation (failure_count): │ - │ │ 1: retry │ - │ │ → lateral_plan 주입 │ - │ │ → 새 아이템 → Pending │ - │ │ → worktree 보존 │ - │ │ → on_fail 실행 안 함 │ - │ │ │ + │ Completed │ │ Escalation 정책 적용 │ + │ │ │ (history 기반 count) │ + │ handler 완료 │ │ │ + │ evaluate 대기 │ │ 1: retry │ + │ │ │ → 새 아이템 → Pending │ + │ force_trigger │ │ → worktree 보존 │ + │ ("evaluate") │ │ → on_fail 실행 안 함 │ + └────────┬────────┘ │ │ │ │ 2: retry_with_comment │ - │ │ → lateral_plan 주입 │ │ │ → on_fail script 실행 │ │ │ → 새 아이템 → Pending │ │ │ → worktree 보존 │ │ │ │ │ │ 3: hitl │ - │ │ → lateral_report 첨부 │ │ │ → on_fail script 실행 │ │ │ → HITL 이벤트 생성 ───────┐│ │ │ → worktree 보존 ││ @@ -126,7 +76,7 @@ QueueItem::builder() │ │ replan → HITL(replan) ────┤│ │ │ └───────────────────────────────┘│ │ │ │ │ - │ evaluate cron (per-item) │ │ + │ evaluate cron │ │ │ (LLM이 belt queue done/hitl CLI 호출) │ │ │ │ │ ┌────┴────┐ │ │ @@ -189,8 +139,6 @@ QueueItem::builder() failure_count는 append-only history에서 계산한다: `history | filter(state, failed) | count`. on_enter 실패도 handler 실패와 동일하게 failure_count에 포함된다. -> **v6 (#723)**: 모든 실패에서 StagnationDetector가 CompositeSimilarity로 유사도 분석을 수행한다. 패턴이 감지되면 LateralAnalyzer가 내장 페르소나(HACKER, ARCHITECT 등)로 대안 접근법을 분석하고, lateral_plan을 생성하여 retry 시 handler prompt에 주입한다. escalation 자체는 기존 failure_count 기반 그대로이되, **모든 retry가 lateral plan으로 강화**된다. 상세: [Stagnation Detection](./stagnation.md) - --- ## Evaluate 원칙 @@ -201,23 +149,7 @@ failure_count는 append-only history에서 계산한다: `history | filter(state 2. **"충분한가?"만 판단** — "이 handler의 결과물이 다음 단계로 넘어가기에 충분한가?"만 본다. 품질 판단(좋은 코드인가?)은 Cron 품질 루프가 담당한다. -3. **state별 구체 기준은 agent-workspace rules에 위임** — `~/.belt/agent-workspace/.claude/rules/classify-policy.md`에 state별 Done 조건을 정의한다. 코어는 rules를 모르고, `belt agent`가 rules를 참조하여 판단한다. - -### Per-Item 판정 (v6 #722) - -evaluate는 **per-work_id 단위**로 LLM 판정을 실행한다. 각 Completed 아이템에 대해 개별 프롬프트를 발행하고, 해당 아이템의 context를 포함한다. - -``` -for item in queue.get(Completed): - belt_agent_p(workspace, - "아이템 {work_id}의 완료 여부를 판단해줘. - belt context {work_id} --json 으로 컨텍스트를 확인하고, - belt queue done {work_id} 또는 belt queue hitl {work_id} 를 실행해줘") -``` - -- 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템 판정에 영향 없다 -- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비한다 — 별도 batch 제어 없음 -- 기존 `eval_failure_counts`는 이미 per-work_id로 관리됨 (설계 의도 일치) +3. **state별 구체 기준은 agent-workspace rules에 위임** — `~/.belt/agent-workspace/.claude/rules/classify-policy.md`에 state별 Done 조건을 정의한다 (Agent 워크스페이스의 rules 파일, [Agent 워크스페이스](./agent-workspace.md) 참조). 코어는 rules를 모르고, `belt agent`가 rules를 참조하여 판단한다. ### 실패 원칙 @@ -232,14 +164,9 @@ Completed는 **안전한 대기 상태**. evaluate가 실패하든 CLI가 실패 --- -## 수용 기준 - -### Phase 전이 캡슐화 (#718) +--- -- [ ] `QueueItem.phase` 필드는 `pub(crate)` 가시성으로, belt-core 외부에서 직접 대입 불가 -- [ ] 모든 phase 변경은 `QueueItem::transit(to)` 메서드를 경유한다 -- [ ] `transit()` 메서드는 내부에서 `can_transition_to()` 검증 + `updated_at` 갱신을 수행한다 -- [ ] 테스트 코드에서도 phase 직접 대입 대신 `transit()` 또는 테스트 헬퍼를 사용한다 +## 수용 기준 ### 상태 전이 규칙 @@ -255,13 +182,9 @@ Completed는 **안전한 대기 상태**. evaluate가 실패하든 CLI가 실패 - [ ] failure_count=2일 때 `retry_with_comment`가 적용되면 on_fail 실행 후 새 아이템으로 재시도한다 - [ ] failure_count=3일 때 `hitl`이 적용되면 on_fail 실행 후 HITL 이벤트가 생성된다 - [ ] on_enter 실패도 failure_count에 포함된다 -- [ ] 모든 실패에서 stagnation 분석이 실행되고, 패턴 감지 시 lateral_plan이 retry에 주입된다 -### Evaluate (per-item, #722) +### Evaluate 실패 시 재시도 -- [ ] evaluate는 per-work_id 단위로 LLM 판정을 실행한다 -- [ ] 각 판정에 해당 아이템의 context가 포함된다 -- [ ] 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템에 영향 없다 - [ ] evaluate LLM 오류 시 아이템은 Completed에 머무르고, 다음 cron tick에서 재시도된다 - [ ] evaluate 반복 실패(N회)로 HITL 에스컬레이션 시 HitlReason::EvaluateFailure가 기록된다 - [ ] on_done script 실패 시 Failed 전이되고, on_fail은 실행하지 않는다 @@ -277,11 +200,8 @@ Completed는 **안전한 대기 상태**. evaluate가 실패하든 CLI가 실패 ### 관련 문서 -- [DESIGN-v6](../DESIGN-v6.md) — 설계 철학 -- [Daemon](./daemon.md) — 내부 모듈 구조 + 실행 루프 -- [Stagnation Detection](./stagnation.md) — 반복 패턴 감지 + lateral thinking -- [LifecycleHook](./lifecycle-hook.md) — 상태 전이 반응 trait -- [DataSource](./datasource.md) — 수집/컨텍스트 + escalation 정책 +- [DESIGN-v5](../DESIGN-v5.md) — 설계 철학 +- [DataSource](./datasource.md) — escalation 정책 + on_fail script - [Cron 엔진](./cron-engine.md) — evaluate cron + force_trigger - [실패 복구와 HITL](../flows/04-failure-and-hitl.md) — 실패/HITL 시나리오 - [Data Model](./data-model.md) — 테이블 스키마, 도메인 enum diff --git a/spec/draft/flows/01-setup.md b/spec/archive/v5/flows/01-setup.md similarity index 79% rename from spec/draft/flows/01-setup.md rename to spec/archive/v5/flows/01-setup.md index ec7e651..2257e2f 100644 --- a/spec/draft/flows/01-setup.md +++ b/spec/archive/v5/flows/01-setup.md @@ -66,9 +66,6 @@ sources: 3: hitl terminal: skip # hitl timeout 시 적용 (skip 또는 replan) - # 참고: on_done/on_fail은 v6 Phase 1에서 ScriptLifecycleHook 어댑터로 처리됨. - # Phase 2에서 DataSource별 LifecycleHook impl로 대체 예정. - runtime: default: claude claude: @@ -90,30 +87,6 @@ workspace는 하나의 외부 레포와 1:1로 대응한다. GitHub 기준으로 6. Agent 워크스페이스 초기화 확인 ``` -### 에러 시나리오 - -CLI가 등록 전에 즉시 검증하고, 실패 시 구체적 에러 메시지를 표시한다. - -``` -belt workspace add --config workspace.yaml - - yaml 파싱 실패: - → "Error: workspace.yaml:12 — 'handlers' 필드가 필요합니다" - - repo 접근 불가: - → "Error: https://github.com/org/repo 접근 불가 (401 Unauthorized)" - → "hint: gh auth status로 인증 상태를 확인하세요" - - workspace 이름 중복: - → "Error: workspace 'auth-project'가 이미 존재합니다" - → "hint: belt workspace remove auth-project로 기존 workspace를 삭제하세요" - - DataSource 유형 미지원: - → "Error: 'jira' DataSource는 아직 지원되지 않습니다 (v7+)" -``` - -모든 검증은 DB 기록 전에 수행된다. 실패 시 부수효과 없음. - --- ## 2. 컨벤션 부트스트랩 diff --git a/spec/draft/flows/02-spec-lifecycle.md b/spec/archive/v5/flows/02-spec-lifecycle.md similarity index 65% rename from spec/draft/flows/02-spec-lifecycle.md rename to spec/archive/v5/flows/02-spec-lifecycle.md index 347f99d..60025f2 100644 --- a/spec/draft/flows/02-spec-lifecycle.md +++ b/spec/archive/v5/flows/02-spec-lifecycle.md @@ -7,22 +7,21 @@ ## Spec Lifecycle ``` -Draft ──→ Analyzing ──→ Active ←──→ Paused - │ │ - │ 문제 발견 ▼ - └→ Draft Completing - │ - ▼ - Completed (terminal) +Draft ──→ Active ←──→ Paused + │ + ▼ + Completing + │ + ▼ + Completed (terminal) Any ──→ Archived (soft delete) Archived ──resume──→ Active (복구) ``` -| 상태 | 가능한 전이 | CLI / 트리거 | -|------|------------|-------------| -| Draft | → Analyzing | `spec add` | -| Analyzing | → Active(분석 통과), → Draft(문제 발견) | 자동 (LLM 분석) | +| 상태 | 가능한 전이 | CLI | +|------|------------|-----| +| Draft | → Active | `spec add` | | Active | → Paused, → Completing(자동), → Archived | `spec pause`, `spec remove` | | Paused | → Active, → Archived | `spec resume`, `spec remove` | | Completing | → Active(gap 발견), → Completed(HITL 승인) | 자동 | @@ -31,50 +30,17 @@ Archived ──resume──→ Active (복구) --- -## 등록 → 분석 → 이슈 분해 +## 등록 → 이슈 분해 ``` /spec add [file] - → 필수 섹션 검증 (기계적: 개요, 요구사항, 아키텍처, 테스트, 수용 기준) - → DB에 저장 (status: Analyzing) - │ - ▼ -Analyzing: - LLM이 스펙을 분석 (설정: ~/.belt/config.yaml) - ├── quality — 섹션 완성도, AC 구체성 - ├── decomposability — 이슈로 분해 가능한지 - └── dependency — 기존 스펙과 충돌/의존 - │ - ├── 분석 점수 >= auto_approve_threshold → Active로 자동 전이 - ├── 점수 미달 → 사용자에게 피드백 + Draft로 복귀 - │ "AC #3이 검증 불가능합니다. 구체적 기대 결과를 추가하세요" - │ "스펙 'auth-v2'와 의존 관계가 감지되었습니다" - └── 분석 실패 (LLM 오류) → Draft로 복귀 + 에러 표시 - │ - ▼ -Active: + → 필수 섹션 검증 (개요, 요구사항, 아키텍처, 테스트, 수용 기준) + → DB에 저장 (status: Active) → 스펙 분해 → 이슈 자동 생성 → 각 이슈에 trigger 라벨 (예: belt:analyze) 부착 → DataSource.collect()가 감지 → 파이프라인 진입 ``` -### 분석 설정 - -`~/.belt/config.yaml`에서 글로벌 기본값을 정의하고, workspace yaml에서 오버라이드 가능하다. - -```yaml -# ~/.belt/config.yaml -spec: - analysis: - runtime: claude # 분석에 사용할 LLM - model: sonnet - checks: # 활성화할 분석 항목 - - quality # 섹션 완성도, AC 구체성 - - decomposability # 이슈로 분해 가능한지 - - dependency # 다른 스펙과 충돌/의존 - auto_approve_threshold: 0.8 # 이 점수 이상이면 자동으로 Active 전이 -``` - ### 스펙 분해 전략 스펙 분해는 **built-in skill**로 제공되며, 커스텀 오버라이드가 가능하다. diff --git a/spec/archive/v5/flows/03-issue-pipeline.md b/spec/archive/v5/flows/03-issue-pipeline.md new file mode 100644 index 0000000..b15975a --- /dev/null +++ b/spec/archive/v5/flows/03-issue-pipeline.md @@ -0,0 +1,100 @@ +# Flow 3: 이슈 파이프라인 — 컨베이어 벨트 + +> 이슈가 DataSource의 상태 정의에 따라 자동으로 처리되고, Done이 다음 단계를 트리거한다. + +--- + +## 컨베이어 벨트 흐름 + +``` +belt:analyze 감지 → [analyze handlers] → evaluate → on_done script → belt:implement 부착 + │ +belt:implement 감지 → [implement handlers] → evaluate → on_done script → belt:review 부착 + │ +belt:review 감지 → [review handlers] → evaluate → on_done script → belt:done 부착 +``` + +각 구간은 독립적인 QueueItem. 되돌아가지 않고, 항상 새 아이템으로 다음 구간에 진입. + +--- + +## 단일 구간 상세 + +``` +DataSource.collect(): trigger 조건 매칭 (예: belt:analyze 라벨) + │ + ▼ + Pending → Ready → Running (자동 전이, concurrency 제한) + │ + │ ① worktree 생성 (인프라, 또는 retry 시 기존 보존분 재사용) + │ ② on_enter script 실행 (정의된 경우) + │ ③ handlers 순차 실행: + │ prompt → AgentRuntime.invoke() (worktree 안에서) + │ script → bash (WORK_ID + WORKTREE 주입) + │ + ├── 전부 성공 → Completed + │ │ + │ ▼ + │ evaluate cron (force_trigger로 즉시 실행 + 주기 폴링 하이브리드): + │ "완료? 추가 검토?" + │ ├── Done → on_done script 실행 → worktree 정리 + │ │ └── script 실패 → Failed (로그 기록, 재시도 가능) + │ └── HITL → HITL 이벤트 생성 → 사람 대기 (worktree 보존) + │ + └── 실패 (handler 또는 on_enter) → escalation 정책 적용 + ├── retry → 새 아이템 생성, worktree 보존 + ├── retry_with_comment → on_fail + 새 아이템 생성 + └── hitl → on_fail + HITL 생성 + └── 사람: done/retry/skip/replan + └── timeout → terminal (skip 또는 replan) +``` + +--- + +## on_done script 예시 + +on_done script는 `belt context`로 필요한 정보를 조회하여 외부 시스템에 결과를 반영한다. + +```yaml +on_done: + - script: | + CTX=$(belt context $WORK_ID --json) + ISSUE=$(echo $CTX | jq -r '.issue.number') + REPO=$(echo $CTX | jq -r '.source.url') + TITLE=$(echo $CTX | jq -r '.issue.title') + gh pr create --title "$TITLE" --body "Closes #$ISSUE" -R $REPO + gh issue edit $ISSUE --remove-label "belt:implement" -R $REPO + gh issue edit $ISSUE --add-label "belt:review" -R $REPO +``` + +Daemon이 주입하는 환경변수는 `WORK_ID`와 `WORKTREE`뿐. 이슈 번호, 레포 URL 등은 `belt context`로 직접 조회한다. + +--- + +## 피드백 루프 + +### PR review comment (changes-requested) + +``` +DataSource.collect()가 changes-requested 감지 + → 새 아이템 생성 → handlers 실행 → 수정 반영 +``` + +### /spec update + +``` +스펙 변경 → on_spec_active → Cron(gap-detection) 재평가 + → gap 발견 시 새 이슈 생성 → 파이프라인 재진입 +``` + +### 핵심 원칙 + +**스펙 = 계약**. 계약이 바뀌어야 하면 `/spec update`. 계약 범위 내 작업이면 이슈 등록. + +--- + +### 관련 문서 + +- [DataSource](../concerns/datasource.md) — 상태 기반 워크플로우 + context 스키마 +- [실패 복구와 HITL](./04-failure-and-hitl.md) — escalation 정책 +- [Cron 엔진](../concerns/cron-engine.md) — evaluate cron + 품질 루프 diff --git a/spec/archive/v5/flows/04-failure-and-hitl.md b/spec/archive/v5/flows/04-failure-and-hitl.md new file mode 100644 index 0000000..9869b75 --- /dev/null +++ b/spec/archive/v5/flows/04-failure-and-hitl.md @@ -0,0 +1,128 @@ +# Flow 4: 실패 복구와 HITL + +> handler 실행 실패 시 escalation 정책에 따라 복구하고, evaluate가 HITL로 분류하면 사람의 판단을 요청한다. + +--- + +## 실패 경로 + +``` +handler 또는 on_enter 실행 실패 + │ + ▼ +escalation 정책 적용 (failure_count 기반, history에서 계산): + │ + ├── retry → 조용히 재시도 (on_fail 실행 안 함, worktree 보존) + ├── retry_with_comment → on_fail script 실행 + 재시도 (worktree 보존) + └── hitl → on_fail script 실행 + HITL 이벤트 생성 (worktree 보존) + └── 사람 응답: done / retry / skip / replan + └── timeout → terminal 액션 적용 (설정에 따라 skip 또는 replan) +``` + +`retry`만 on_fail을 실행하지 않는다. "조용한 재시도"로 외부 시스템에 노이즈를 주지 않는다. + +> **skip/replan**: 독립적인 escalation level이 아니라 hitl의 응답 경로 또는 hitl timeout 시의 `terminal` 설정이다. skip은 terminal 상태(Skipped)이므로, 이후 실패가 발생할 수 없어 순차적 escalation level로 정의할 수 없다. + +--- + +## Escalation 정책 (workspace yaml 소유) + +```yaml +sources: + github: + escalation: + 1: retry + 2: retry_with_comment + 3: hitl + terminal: skip # hitl timeout 시 적용 (skip 또는 replan) +``` + +DataSource마다 다른 정책이 가능한 구조. v5는 GitHub 정책만 구현. + +### on_fail script 예시 + +```yaml +on_fail: + - script: | + CTX=$(belt context $WORK_ID --json) + ISSUE=$(echo $CTX | jq -r '.issue.number') + REPO=$(echo $CTX | jq -r '.source.url') + FAILURES=$(echo $CTX | jq '[.history[] | select(.status=="failed")] | length') + gh issue comment $ISSUE --body "실패 (시도 횟수: $FAILURES)" -R $REPO +``` + +failure_count는 별도 컬럼이 아니라 history의 append-only 이벤트에서 계산. + +--- + +## HITL (Human-in-the-Loop) + +### 생성 경로 + +| 경로 | 트리거 | +|------|--------| +| Escalation | handler 또는 on_enter 실패 → escalation 정책이 HITL 결정 | +| evaluate | handler 성공 → evaluate가 "사람이 봐야 한다" 판단 | +| 스펙 완료 | 모든 linked issues Done → 최종 확인 요청 | +| 충돌 | DependencyGuard가 스펙 충돌 감지 | + +> **DependencyGuard**: 다중 스펙 등록 시 Agent가 기존 Active 스펙과의 충돌/의존성을 분석하여 등록하는 가드. 같은 파일/모듈에 영향을 주는 스펙이 동시에 진행될 때 HITL 이벤트를 생성한다. 상세는 [스펙 생명주기](./02-spec-lifecycle.md)의 "다중 스펙 우선순위" 참조. + +### 응답 경로 + +``` +사용자 응답 (TUI / CLI / /agent 세션) + → belt hitl respond --choice N + → 라우팅: + "done" → on_done script 실행 + ├── script 성공 → Done (worktree 정리) + └── script 실패 → Failed (worktree 보존, 로그 기록) + "retry" → 새 아이템 생성 → Pending (worktree 보존) + "skip" → Skipped (worktree 정리) + "replan" → replan 처리 (아래 참조) +``` + +### Replan + +replan은 반복 실패한 아이템의 접근 방식을 재설계하기 위한 경로이다. + +``` +replan 요청 (사람 응답 또는 hitl timeout) + → replan_count 증가 (max 3) + → max 초과 시: Skipped (worktree 정리, 더 이상 재시도 불가) + → max 이내: + 1. HitlReason::SpecModificationProposed 이벤트 생성 + 2. Failed 전이 (worktree 보존) + 3. Agent가 실패 컨텍스트(history, 에러 로그)를 분석하여 스펙 수정 제안 + 4. 사용자가 /spec update로 스펙 수정 → 새 이슈 생성 → 파이프라인 재진입 +``` + +### 타임아웃 + +``` +기본: 24시간 +초과 시: hitl-timeout cron (5분 주기)이 감지 + → escalation.terminal 설정에 따라: + skip → Skipped (worktree 정리) + replan → replan 처리 (위 참조) +``` + +--- + +## Graceful Shutdown + +``` +SIGINT → on_shutdown: + 1. Running 아이템 완료 대기 (timeout: 30초) + → timeout 초과: Pending으로 롤백, worktree 보존 + (재시작 후 기존 worktree를 재사용하여 이어서 진행) + 2. Cron engine 정지 +``` + +--- + +### 관련 문서 + +- [DataSource](../concerns/datasource.md) — escalation 정책 + on_fail script +- [Agent](../concerns/agent-workspace.md) — 대화형 에이전트 (HITL 응답 경로 포함) +- [이슈 파이프라인](./03-issue-pipeline.md) — 실패가 발생하는 실행 흐름 diff --git a/spec/draft/flows/05-monitoring.md b/spec/archive/v5/flows/05-monitoring.md similarity index 80% rename from spec/draft/flows/05-monitoring.md rename to spec/archive/v5/flows/05-monitoring.md index 4cad28f..2bd6fca 100644 --- a/spec/draft/flows/05-monitoring.md +++ b/spec/archive/v5/flows/05-monitoring.md @@ -46,12 +46,11 @@ │ ⚠ #39 Auth refactor (failed) ││ evaluate 대기 │ │ ⏳ #46 Missing tests ││ │ ├──────────────────────────────────────┤├────────────────────────┤ -│ Orphan: 0 | HITL: 1 | Stag: 0 ││ Logs │ +│ Orphan: 0 | HITL: 1 pending ││ Logs │ │ Kanban: 0P | 0Re | 1Ru | 1C | 2D ││ ... │ └──────────────────────────────────────┘└────────────────────────┘ ``` -> **v6**: Stagnation 카운터 추가 — 현재 stagnation이 감지된 아이템 수를 표시. > **Kanban 약어**: P=Pending, Re=Ready, Ru=Running, C=Completed, D=Done, H=HITL, S=Skipped, F=Failed ### 전이 타임라인 (ItemDetail 오버레이, Enter) @@ -74,32 +73,24 @@ └───────────────────────────────────────────────┘ ``` -### 실패 + Lateral Thinking 타임라인 (v6) +### 실패 아이템 타임라인 ``` ┌─ #39 Auth refactor ──────────────────────────┐ -│ Phase: HITL | Runtime: claude/sonnet │ -│ Stagnation: SPINNING (score: 0.95) │ +│ Phase: Failed | Runtime: claude/sonnet │ │ │ │ Timeline: │ │ 13:00 ○ Pending ← github.collect() │ -│ 13:00 ○ Running (attempt 1) │ -│ └ handler: compile error │ -│ 13:08 ⟳ SPINNING detected (score: 0.95) │ -│ └ lateral: HACKER 페르소나 │ -│ └ plan: tower-sessions crate 시도 │ -│ 13:08 ○ Running (attempt 2, lateral) │ -│ └ handler: 다른 에러 (progress!) │ -│ 13:18 ⟳ retry_with_comment (2/3) │ -│ └ lateral: CONTRARIAN 페르소나 │ -│ └ plan: trait object 접근 │ -│ 13:18 ○ Running (attempt 3, lateral) │ -│ └ handler: 컴파일 성공, 테스트 실패 │ -│ 13:28 ● HITL (3/3) │ -│ └ lateral report 첨부 │ -│ └ 2회 사고 전환 후에도 미해결 │ +│ 13:00 ○ Running │ +│ └ handler: claude/sonnet (2.1K, 8m) │ +│ 13:08 ○ Completed │ +│ 13:08 ○ evaluate → Done │ +│ 13:08 ✗ on_done script (exit 1, 0.5s) │ +│ └ error: gh pr create rate limited │ +│ 13:08 ● Failed │ +│ └ worktree 보존: /tmp/belt/auth-39 │ │ │ -│ Actions: [d] done [r] retry [s] skip │ +│ Actions: [r] retry-script [s] skip │ └───────────────────────────────────────────────┘ ``` @@ -136,13 +127,12 @@ ● belt daemon (uptime 2h 15m) Workspaces: - auth-project ● active queue: 1P 1R 1C 2D 1F specs: 2/3 stag: 0 + auth-project ● active queue: 1P 1R 1C 2D 1F specs: 2/3 backend-tasks ● active queue: 0P 0R 0C 5D specs: 1/1 ✓ Runtime: claude/sonnet (45.2K tokens/1h) HITL: 1 pending ⚠ Failed: 1 ⚠ -Stagnation: 0 Next evaluate: 25s ``` @@ -189,8 +179,6 @@ Dependencies: │ on_enter 3 ok │ │ evaluate 12 ok 1 hitl │ │ ⚠ on_done 1 failed │ -│ stagnation 2 detected │ -│ lateral 2 plans │ └──────────────────────────────────┘ ``` @@ -207,22 +195,43 @@ HITL 이벤트가 생성되면 사용자에게 다음 경로로 알린다: | /agent 세션 | 진입 시 HITL 대기 목록 자동 표시 | | on_fail script | escalation=hitl 시 실행 — GitHub 코멘트 등으로 외부 알림 가능 | -별도 push 알림(Slack, email)은 LifecycleHook impl에서 처리한다 (webhook 호출 등). - -> **v6**: Stagnation으로 HITL에 진입한 경우, lateral report(시도한 접근법들, 각 분석 결과)가 표시되어 사용자가 지금까지의 접근 전환 이력을 참고할 수 있다. +별도 push 알림(Slack, email)은 on_fail/on_done script에서 직접 구현한다 (webhook 호출 등). --- ## 6. 데이터 요구사항 -모든 전이 이벤트와 토큰 사용량은 DB에 기록된다. 스키마 상세: [Data Model](../concerns/data-model.md) +```sql +-- 아이템별 전이 이벤트 (append-only) +transition_events ( + id TEXT PRIMARY KEY, + work_id TEXT NOT NULL, + source_id TEXT NOT NULL, -- 계보 추적 + event_type TEXT NOT NULL, -- phase_enter, handler, evaluate, on_done, on_fail, on_enter + phase TEXT, -- Pending, Ready, Running, Completed, Done, HITL, Failed, Skipped + detail TEXT, -- script exit code, prompt result, error message + created_at TEXT NOT NULL +) + +-- token 사용량 (AgentRuntime 실행마다 기록) +token_usage ( + id TEXT PRIMARY KEY, + work_id TEXT NOT NULL, + workspace TEXT NOT NULL, + runtime TEXT NOT NULL, -- claude, gemini, codex + model TEXT, -- sonnet, opus, haiku + input_tokens INTEGER, + output_tokens INTEGER, + duration_ms INTEGER, + created_at TEXT NOT NULL +) +``` --- ### 관련 문서 -- [DESIGN-v6](../DESIGN-v6.md) — QueuePhase 상태 머신 -- [Stagnation Detection](../concerns/stagnation.md) — 반복 패턴 감지 시각화 +- [DESIGN-v5](../DESIGN-v5.md) — QueuePhase 상태 머신 - [스펙 생명주기](./02-spec-lifecycle.md) — 스펙 진행률 - [실패 복구와 HITL](./04-failure-and-hitl.md) — HITL 오버레이 - [CLI 레퍼런스](../concerns/cli-reference.md) — 전체 커맨드 트리 diff --git a/spec/concerns/agent-workspace.md b/spec/concerns/agent-workspace.md index 2437558..b0ecf7b 100644 --- a/spec/concerns/agent-workspace.md +++ b/spec/concerns/agent-workspace.md @@ -10,27 +10,30 @@ 분류 로직은 코어에 속한다. Agent와 무관. +**v6 (#722)**: evaluate는 **per-work_id 단위**로 LLM 판정을 실행한다. + ``` handler 전부 성공 → Completed │ ▼ -evaluate cron (force_trigger로 즉시 실행 가능): - belt agent -p "Completed 아이템의 완료 여부를 판단해줘" - │ - │ LLM이 belt context로 컨텍스트 조회 후 CLI 도구로 결정: +Evaluator (Daemon tick에서 Executor보다 먼저 실행): + Progressive Pipeline: + Stage 1: Mechanical (cargo test 등, 비용 0) + → 실패 시 Retry (LLM 안 부름) + Stage 2: Semantic (LLM 1회, belt agent -p) + → LLM이 belt context로 컨텍스트 조회 후 판정 │ - ├── belt queue done $WORK_ID - │ → Daemon이 on_done script 실행 (CLI 명령이 상태 전이를 트리거하고, Daemon이 script를 실행) - │ ├── script 성공 → Done (worktree 정리) - │ └── script 실패 → Failed (worktree 보존, 로그 기록) + ├── Done → hook.on_done() 트리거 + │ ├── hook 성공 → Done (worktree 정리) + │ └── hook 실패 → Failed (worktree 보존) │ - └── belt queue hitl $WORK_ID --reason "..." - → HITL 이벤트 생성 → 사람 대기 (worktree 보존) + └── HITL → HITL 이벤트 생성 → 사람 대기 (worktree 보존) ``` -evaluate cron: `interval 60s + force_trigger on Completed 전이`. LLM이 JSON을 파싱하는 게 아니라, 직접 `belt queue done/hitl` CLI를 호출하여 상태를 전이한다. - -evaluate의 판단 입력: `belt context $WORK_ID --json` (queue 메타데이터 + 외부 시스템 컨텍스트 + append-only history). +- Evaluator는 Daemon tick의 정규 단계. 상세: [Evaluator](./evaluator.md) +- SemanticStage에서 LLM이 `belt queue done/hitl` CLI를 직접 호출하여 상태를 전이한다 +- 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템 판정에 영향 없다 +- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비한다 --- @@ -181,14 +184,9 @@ v4 (15개) → v5 (3개): ### classify-policy.md 로딩 경로 및 해석 (R-CW-007) `classify-policy.md`는 LLM 에이전트가 큐 아이템을 Done / HITL로 분류할 때 -참조하는 정책 문서다. 두 가지 형태로 존재한다: +참조하는 자연어 정책 문서다. `.claude/rules/` 하위에 위치하며, system prompt에 주입된다. -| 파일 | 위치 | 소비자 | 용도 | -|------|------|--------|------| -| `classify-policy.md` | `.claude/rules/` 하위 | LLM agent (system prompt) | 자연어 분류 기준 | -| `classify-policy.yaml` | workspace root | daemon evaluator | machine-readable 라우팅 규칙 | - -#### 로딩 경로 (classify-policy.md) +#### 로딩 경로 `agent::resolve_rules_dir` 함수가 아래 우선순위로 **디렉토리**를 탐색한다. 첫 번째로 존재하는 디렉토리 안의 **모든 `.md` 파일**이 로드된다. @@ -203,11 +201,9 @@ Priority 3: $BELT_HOME/claw-workspace/.claude/rules/ (global, belt claw init) #### 파일 미존재 시 fallback -- 디렉토리 자체가 없는 경우: agent는 built-in Claw rules(대화 턴 제한, 응답 포맷, - 에러 핸들링)만으로 실행. 에러 없음. +- 디렉토리 자체가 없는 경우: agent는 built-in Claw rules(대화 턴 제한, 응답 포맷, 에러 핸들링)만으로 실행. 에러 없음. - 디렉토리는 있지만 `.md` 파일이 없는 경우: 동일하게 built-in rules만 사용. -- `classify-policy.md`만 없고 다른 `.md`가 있는 경우: 다른 정책 파일은 정상 로드, - 분류 정책 가이던스만 빠진 채 실행. +- `classify-policy.md`만 없고 다른 `.md`가 있는 경우: 다른 정책 파일은 정상 로드, 분류 정책 가이던스만 빠진 채 실행. #### 구현 위치 @@ -227,19 +223,22 @@ Priority 3: $BELT_HOME/claw-workspace/.claude/rules/ (global, belt claw init) | `belt hitl list --json` | HITL 목록 조회 | 대화형 세션 | | `belt queue list --json` | 큐 목록 조회 | 대화형 세션 | -### evaluate cron과의 관계 +### Evaluator와의 관계 -evaluate cron은 내부적으로 `belt agent --workspace -p ""`를 호출한다. 이때: -- Completed 아이템 목록을 프롬프트에 포함 -- LLM이 `belt context`로 각 아이템 정보를 조회 +Evaluator의 SemanticStage가 내부적으로 `belt agent -p`를 호출한다. 이때: +- **per-item**: 각 아이템에 대해 개별 프롬프트 발행 (v6 #722) +- LLM이 `belt context $WORK_ID`로 해당 아이템 정보를 조회 - 판단 후 `belt queue done/hitl` CLI를 직접 호출하여 상태 전이 - classify-policy.md의 state별 Done 조건이 판단 기준 +- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비 + +상세: [Evaluator](./evaluator.md) --- ### 관련 문서 -- [DESIGN-v5](../DESIGN-v5.md) — QueuePhase 상태 머신 + evaluate 위치 +- [DESIGN-v6](../DESIGN-v6.md) — QueuePhase 상태 머신 + evaluate 위치 - [CLI 레퍼런스](./cli-reference.md) — CLI 전체 커맨드 트리 - [Cron 엔진](./cron-engine.md) — evaluate cron - [Data Model](./data-model.md) — 컨텍스트 모델 (belt context 출력) diff --git a/spec/concerns/cron-engine.md b/spec/concerns/cron-engine.md index b1a26a8..2c140b5 100644 --- a/spec/concerns/cron-engine.md +++ b/spec/concerns/cron-engine.md @@ -9,7 +9,9 @@ ``` 1. 인프라 유지 — hitl-timeout, log-cleanup, daily-report (결정적) -2. 품질 루프 — evaluate, gap-detection, knowledge-extract (LLM 사용) +2. 품질 루프 — gap-detection, knowledge-extract (LLM 사용) + +※ evaluate는 Daemon tick 루프의 정규 단계로 이동. 상세: [Evaluator](./evaluator.md) ``` --- @@ -57,7 +59,6 @@ gap 발견 → DataSource에서 open 아이템 조회 (Pending/Ready/Running) | Job | 주기 | 동작 | |-----|------|------| -| evaluate | 60초 | 완료 아이템 분류 (Done or HITL) — `belt agent -p` | | gap-detection | 1시간 | 스펙-코드 대조, gap 발견 시 이슈 생성 | | knowledge-extract | 1시간 | merged PR 지식 추출 | @@ -70,39 +71,19 @@ gap 발견 → DataSource에서 open 아이템 조회 (Pending/Ready/Running) --- -## Force Trigger (하이브리드 실행 모델) +## Force Trigger -evaluate는 **cron 주기 폴링 + force_trigger 즉시 실행**의 하이브리드 모델로 동작한다: +force_trigger는 cron job을 다음 tick에서 우선 실행하도록 스케줄링한다. ``` -1. 주기 폴링: evaluate cron이 60초마다 Completed 아이템을 스캔 -2. 즉시 실행: handler 성공 → Completed 전이 → force_trigger("evaluate") - → last_run_at = NULL → 다음 tick(10초 이내)에서 즉시 실행 +force_trigger(job_name): + job.last_run_at = NULL → 다음 tick에서 즉시 실행 ``` -- **force_trigger**는 동기적으로 evaluate를 실행하지 않는다. cron의 `last_run_at`을 리셋하여 다음 tick에서 우선 실행되도록 스케줄링한다. -- evaluate LLM 호출도 concurrency slot을 소비한다 (`daemon.max_concurrent`에 `active_evaluate_count`가 포함됨). -- 주기 폴링은 force_trigger가 누락된 경우(예: 프로세스 재시작)의 안전망 역할을 한다. - -> handler 실패 시에는 force_trigger 없이 즉시 escalation 정책이 적용된다 (evaluate 불필요). - ---- - -## 스크립트 구조 - -Cron job의 스크립트도 `belt context`를 활용하여 필요한 정보를 조회한다. +- 동기적으로 실행하지 않는다. cron의 `last_run_at`을 리셋할 뿐. +- gap-detection 등 품질 루프 job에 사용. -```bash -#!/bin/bash -# Guard: Completed 상태 아이템 있을 때만 -COMPLETED=$(belt queue list --workspace "$WORKSPACE" --phase completed --json | jq 'length') -if [ "$COMPLETED" = "0" ]; then exit 0; fi - -# 실행: evaluate -# LLM이 context를 조회하고 belt queue done/hitl CLI를 직접 호출 -belt agent --workspace "$WORKSPACE" -p \ - "Completed 아이템의 완료 여부를 판단하고, belt queue done 또는 belt queue hitl 을 실행해줘" -``` +> **evaluate는 v6에서 Daemon tick 정규 단계로 이동**. Completed 아이템은 Evaluator가 다음 tick에서 Progressive Pipeline으로 판정한다. 상세: [Evaluator](./evaluator.md) --- @@ -132,6 +113,7 @@ Cron 스크립트에는 workspace 정보가 필요하므로 추가 변수를 주 ### 관련 문서 -- [DESIGN-v5](../DESIGN-v5.md) — evaluate 아키텍처 +- [DESIGN-v6](../DESIGN-v6.md) — evaluate 아키텍처 - [DataSource](./datasource.md) — belt context 스키마 - [Agent](./agent-workspace.md) — evaluate와 Agent의 관계 +- [Stagnation Detection](./stagnation.md) — handler 실패 시 패턴 감지 (cron이 아닌 동기 실행) diff --git a/spec/concerns/daemon.md b/spec/concerns/daemon.md index bb11140..3833678 100644 --- a/spec/concerns/daemon.md +++ b/spec/concerns/daemon.md @@ -1,7 +1,9 @@ -# Daemon — 상태 머신 + 실행기 +# Daemon — State Machine CPU -> Daemon은 yaml에 정의된 prompt/script를 호출하는 단순 실행기. -> GitHub 라벨, PR 생성 같은 도메인 로직을 모른다. +> Daemon은 상태 머신을 틱마다 순회하며 전이를 결정하고, hook을 트리거하는 CPU. +> handler(prompt/script)를 실행하고, 상태 전이 시 workspace의 LifecycleHook을 트리거한다. +> GitHub 라벨, PR 생성 같은 도메인 로직을 모른다 — hook.on_*()의 Result만 받을 뿐. +> 내부는 Advancer·Executor·HitlService 모듈로 분리. 실패 시 StagnationDetector + LateralAnalyzer가 사고를 전환하여 재시도한다. --- @@ -10,15 +12,84 @@ ``` 1. 수집: DataSource.collect() → Pending에 넣기 2. 전이: Pending → Ready → Running (자동, concurrency 제한) -3. 실행: yaml에 정의된 prompt/script 호출 -4. 완료: handler 성공 → Completed 전이 -5. 분류: evaluate cron이 Completed → Done or HITL 판정 (CLI 도구 호출) -6. 반영: on_done/on_fail script 실행 -7. 스케줄: Cron engine으로 주기 작업 실행 +3. 트리거: Running 진입 시 hook.on_enter() 트리거 +4. 실행: yaml에 정의된 handler(prompt/script) 실행 +5. 완료: handler 성공 → Completed 전이 +6. 분류: evaluate가 Completed → Done or HITL 판정 (per-item) +7. 반응: 상태 전이 시 hook.on_done/on_fail/on_escalation 트리거 +8. 스케줄: Cron engine으로 주기 작업 실행 + +Daemon이 아는 것: 상태 머신 + 언제 어떤 hook을 트리거할지 +Daemon이 모르는 것: hook이 실제로 무엇을 하는지 (Result만 받음) ``` --- +## 내부 모듈 구조 (#717) + +Daemon은 상태 머신을 순회하며 전이를 결정하고 hook을 트리거하는 CPU이다. + +``` +Daemon (CPU) + loop { + collector.collect() + advancer.advance() + executor.execute() // handler 실행 + hook 트리거 + cron_engine.tick() + } +``` + +| 모듈 | 책임 | 소유하는 상태 | +|------|------|-------------| +| **Advancer** | Pending→Ready→Running 전이, dependency gate (DB), conflict 검출 | queue, ConcurrencyTracker | +| **Executor** | handler 실행 + hook 트리거, 실패 시 stagnation 분석 + lateral plan + escalation | ActionExecutor, StagnationDetector, LateralAnalyzer | +| **Evaluator** | Completed → Done/HITL 분류 (per-item, 이미 분리됨) | eval_failure_counts | +| **HitlService** | HITL 응답 처리, timeout 만료, terminal action 적용 | — (DB 직접 조회) | +| **CronEngine** | cron tick, force_trigger (이미 분리됨) | CronJob 목록 | + +### Executor 내부 구조 + +``` +Executor + │ + ├── ActionExecutor handler(prompt/script) 실행 + │ + ├── hook: &dyn LifecycleHook 상태 전이 시 트리거 (실행 책임은 Hook impl) + │ ├── on_enter() + │ ├── on_done() + │ ├── on_fail() + │ └── on_escalation() + │ + ├── StagnationDetector 실패 시 패턴 탐지 + │ └── judge: Box + │ └── CompositeSimilarity + │ ├── ExactHash (w: 0.5) + │ ├── TokenFingerprint (w: 0.3) + │ └── NCD (w: 0.2) + │ + └── LateralAnalyzer 패턴 감지 시 사고 전환 + └── personas/ (include_str! 내장) + hacker.md, architect.md, researcher.md, + simplifier.md, contrarian.md +``` + +### 모듈 간 의존 + +``` +Daemon + ├── Advancer (queue, db, dependency_guard) + ├── Executor (action_executor, stagnation_detector, lateral_analyzer) + ├── Evaluator (workspace_config) + ├── HitlService (db) + └── CronEngine (db) +``` + +- 모듈 간 의존은 trait 또는 함수 파라미터로만 전달 (순환 참조 금지) +- 각 모듈은 독립적으로 단위 테스트 가능 +- StagnationDetector는 `Box` 하나만 의존 (Composite 또는 단일) + +--- + ## Concurrency 제어 두 레벨로 동시 실행을 제어한다: @@ -34,9 +105,7 @@ max_concurrent: 4 - **workspace.concurrency**: workspace yaml 루트에 정의. "이 프로젝트에 동시에 몇 개까지 돌릴까". 모든 source의 아이템 합산 기준. - **daemon.max_concurrent**: "머신 리소스 한계" (evaluate cron의 LLM 호출도 slot을 소비) -> **주의**: `concurrency`는 workspace 루트에 위치한다 (`sources.github` 하위가 아님). 하나의 workspace에 여러 source가 있을 수 있으므로, per-source가 아닌 per-workspace 기준으로 제어한다. - -Daemon은 `Ready → Running` 전이 시 두 제한을 모두 확인한다. +Advancer는 `Ready → Running` 전이 시 두 제한을 모두 확인한다. --- @@ -45,97 +114,178 @@ Daemon은 `Ready → Running` 전이 시 두 제한을 모두 확인한다. ``` loop { // 1. 수집 - for source in workspace.sources: - items = source.collect() - queue.push(Pending, items) + for binding in workspace_bindings: + for source in binding.sources: + items = source.collect() + queue.push(Pending, items) - // 2. 자동 전이 + 실행 (2단계 concurrency 제한) - queue.advance_all(Pending → Ready) - ws_slots = workspace.concurrency - queue.count(Running, workspace) - global_slots = daemon.max_concurrent - queue.count_all(Running) - active_evaluate_count - limit = min(ws_slots, global_slots) - queue.advance(Ready → Running, limit=limit) + // 2. 판정 (Evaluator) — 실행보다 먼저 + // Completed 아이템을 비용 순으로 판정: Mechanical → Semantic → (Consensus) + // Ready 아이템 중 이전 기록으로 판정 가능한 것은 handler 실행 없이 판정 + evaluator.evaluate() + // 3. 자동 전이 (Advancer) + advancer.advance_pending_to_ready() // spec dep gate (DB) + advancer.advance_ready_to_running(limit) // queue dep gate (DB) + concurrency + + // 4. 실행 (Executor) for item in queue.get_new(Running): + binding = lookup_workspace_binding(item) + hook = binding.hook // 이 workspace의 LifecycleHook state = lookup_state(item) - - // worktree 생성 (인프라) worktree = create_or_reuse_worktree(item) + ctx = build_hook_context(item, worktree) - // on_enter (실패 시 handler를 실행하지 않고 escalation 적용) - result = run_actions(state.on_enter, WORK_ID=item.id, WORKTREE=worktree) + // on_enter hook 트리거 (실패 시 handler 건너뛰고 실패 경로) + result = hook.on_enter(&ctx) if result.failed: - failure_count = count_failures(item.source_id, item.state) - escalation = lookup_escalation(failure_count) - if escalation != retry: - run_actions(state.on_fail, WORK_ID=item.id, WORKTREE=worktree) - apply_escalation(item, escalation) + executor.handle_failure(item, hook) continue - // handlers 순차 실행 + // handlers 순차 실행 (lateral_plan 있으면 prompt에 주입) for action in state.handlers: - result = execute(action, WORK_ID=item.id, WORKTREE=worktree) + result = executor.execute(action, WORK_ID=item.id, WORKTREE=worktree, + lateral_plan=item.lateral_plan) if result.failed: - failure_count = count_failures(item.source_id, item.state) // history에서 계산 - escalation = lookup_escalation(failure_count) - if escalation != retry: - run_actions(state.on_fail, WORK_ID=item.id, WORKTREE=worktree) - apply_escalation(item, escalation) // retry: worktree 보존 + executor.handle_failure(item, hook) break else: - // 모든 handler 성공 → Completed - queue.transit(item, Completed) - force_trigger("evaluate") + item.transit(Completed) - // 3. cron tick (evaluate, gap-detection 등) + // 5. cron tick (품질 루프: gap-detection, knowledge-extract 등) cron_engine.tick() } +``` + +### Executor.handle_failure — Stagnation + Lateral + Hook 트리거 + +``` +fn handle_failure(item, hook): + // ① Stagnation Detection (항상 실행) + // 각 PatternDetector가 DB에서 자기 관심사 데이터를 직접 조회 + detections = stagnation_detector.detect(item.source_id, item.state, db) + active = detections.filter(|d| d.detected && d.confidence >= threshold) + + // ② Lateral Plan 생성 (패턴 감지 시) + lateral_plan = None + if active.is_not_empty() && lateral_config.enabled: + tried = db.get_tried_personas(item.source_id, item.state) + persona = select_persona(active[0].pattern, tried) + if persona.is_some(): + lateral_plan = lateral_analyzer.analyze( + detection=active[0], + persona=persona, + workspace=item.workspace_id, + ) + + // ③ transition_events에 stagnation 기록 + record_stagnation_event(item, detections, lateral_plan) + + // ④ Escalation 결정 (failure_count 기반) + failure_count = count_failures(item.source_id, item.state) + escalation = lookup_escalation(failure_count) + + // ⑤ Hook 트리거 — Daemon은 트리거만, 실행 책임은 Hook impl + ctx = build_hook_context(item, worktree) + hook.on_escalation(&ctx, escalation) + + if escalation.should_run_on_fail(): + hook.on_fail(&ctx) + + // ⑥ 상태 전이 + match escalation: + retry | retry_with_comment: + new_item = create_retry_item(item, lateral_plan) + // worktree 보존 + + hitl: + lateral_report = build_lateral_report(item.source_id, item.state) + create_hitl_event(item, reason, hitl_notes=lateral_report) + // worktree 보존 +``` + +### lateral_plan이 handler에 주입되는 방식 + +retry로 생성된 새 아이템이 다시 Running에 진입하면, `lateral_plan`이 handler prompt에 추가 컨텍스트로 주입된다: + +``` +원래 prompt: "이슈를 구현해줘" + +주입 후: + "이슈를 구현해줘 -// evaluate cron (force_trigger 가능): -// LLM이 직접 CLI를 호출하여 상태 전이 (JSON 파싱 불필요) -for item in queue.get(Completed): - belt_agent_p(workspace, "Completed 아이템 $WORK_ID 의 완료 여부를 판단하고, - belt queue done $WORK_ID 또는 belt queue hitl $WORK_ID 를 실행해줘") - // → LLM이 context를 조회하고 판단 후 CLI 실행 - // belt queue done $WORK_ID → on_done script 실행 → Done (worktree 정리) - // └── script 실패 → Failed (worktree 보존) - // belt queue hitl $WORK_ID → HITL 이벤트 생성 (worktree 보존) + ⚠ Stagnation Analysis (attempt 2/3) + Pattern: SPINNING | Persona: HACKER + + 실패 원인: 이전 2회 시도에서 동일한 컴파일 에러 반복 + 대안 접근법: tower-sessions crate 활용 + 실행 계획: 1. Cargo.toml 수정 2. 타입 교체 3. middleware 등록 + 주의: 이전과 동일한 접근은 같은 실패를 반복합니다" ``` --- -## 통합 액션 타입 +## Dependency Gate (#721) + +### Spec Dependency Gate + +`check_dependency_gate()` — Pending→Ready 전이 시 확인. 스펙 간 의존 관계 확인. + +### Queue Dependency Gate + +`check_queue_dependency_gate()` — Ready→Running 전이 시 확인. + +dependency phase 확인은 **DB 조회 기반**: + +``` +1. DB에서 dependency work_id 목록 조회 +2. 각 dependency의 phase를 DB에서 조회 +3. 판정: + - Done → gate open + - DB에 없음 → gate open (orphan 허용) + - 그 외 → gate blocked +``` + +### Conflict Gate + +`check_conflict_gate()` — entry_point 겹침 감지. DB 기반. + +--- -두 가지 실행 단위가 있으며, 사용 위치에 따라 허용 범위가 다르다: +## Handler와 Hook의 분리 + +### Handler — yaml에 정의된 작업 ```yaml -- prompt: "..." # → AgentRuntime.invoke() (LLM, worktree 안에서) -- script: "..." # → bash 실행 (결정적, WORK_ID + WORKTREE 주입) +handlers: + - prompt: "..." # → AgentRuntime.invoke() (LLM, worktree 안에서) + - script: "..." # → bash 실행 (결정적, WORK_ID + WORKTREE 주입) ``` -| 위치 | prompt | script | 설명 | -|------|:------:|:------:|------| -| `handlers` | O | O | 워크플로우 핵심 작업 | -| `on_enter` | X | O | Running 진입 시 사전 작업 | -| `on_done` | X | O | 완료 후 외부 시스템 반영 | -| `on_fail` | X | O | 실패 시 외부 시스템 알림 | +handler는 Daemon Executor가 직접 실행한다. 작업의 핵심 로직. + +### Hook — LifecycleHook trait impl -lifecycle hook(`on_enter`/`on_done`/`on_fail`)은 **script만 허용**한다. 결정적 실행이 보장되어야 하고, LLM 호출은 handler에서만 수행한다. 상세는 [Data Model](./data-model.md#액션-타입) 참조. +| hook | 트리거 시점 | 실행 책임 | +|------|-----------|----------| +| `on_enter` | Running 진입 후, handler 실행 전 | Hook impl | +| `on_done` | evaluate Done 판정 후 | Hook impl | +| `on_fail` | 실패 시 (retry 제외) | Hook impl | +| `on_escalation` | escalation 결정 후 | Hook impl | -script 안에서 `belt context $WORK_ID --json`을 호출하여 필요한 정보를 조회한다. +Daemon은 hook을 트리거만 한다. hook이 실제로 무엇을 하는지 모른다. +상세: [LifecycleHook](./lifecycle-hook.md) --- ## 환경변수 -Daemon이 prompt/script 실행 시 주입하는 환경변수는 **2개만**: - | 변수 | 설명 | |------|------| | `WORK_ID` | 큐 아이템 식별자 | | `WORKTREE` | worktree 경로 | -나머지는 `belt context $WORK_ID --json`으로 조회. 상세는 [DataSource](./datasource.md) 참조. +나머지는 `belt context $WORK_ID --json`으로 조회. --- @@ -145,42 +295,61 @@ Daemon이 prompt/script 실행 시 주입하는 환경변수는 **2개만**: SIGINT → on_shutdown: 1. Running 아이템 완료 대기 (timeout: 30초) → timeout 초과: Pending으로 롤백, worktree 보존 - (재시작 후 해당 아이템이 다시 Ready → Running 전이 시 기존 worktree를 재사용) 2. Cron engine 정지 ``` -> **worktree 보존 원칙**: shutdown 롤백 시 worktree를 정리하지 않는다. retry와 동일하게, 재시작 후 이전 작업 위에서 이어서 진행할 수 있도록 worktree를 보존한다. 좀비 worktree는 `log-cleanup` cron이 TTL 기반으로 정리한다. - ---- - --- ## 수용 기준 -### Concurrency 제어 +### Daemon = CPU + +- [ ] Daemon은 상태 머신 순회 + hook 트리거만 담당한다 +- [ ] 상태 전이 시 workspace의 LifecycleHook.on_*()을 트리거한다 +- [ ] hook의 실행 결과(Result)만 받고, 구체적 동작을 모른다 + +### 내부 모듈 구조 (#717) + +- [ ] phase 전이는 Advancer, handler 실행+hook 트리거+stagnation+lateral은 Executor, HITL은 HitlService +- [ ] 각 모듈은 독립적으로 단위 테스트 가능하다 +- [ ] 모듈 간 의존은 trait 또는 함수 파라미터로만 전달 (순환 참조 금지) + +### Stagnation + Lateral 통합 (#723) + +- [ ] handler/on_enter 실패 시 StagnationDetector가 항상 실행된다 +- [ ] CompositeSimilarity로 outputs/errors를 별도 검사한다 +- [ ] 패턴 감지 시 LateralAnalyzer가 내장 페르소나로 lateral_plan을 생성한다 +- [ ] lateral_plan이 retry 시 handler prompt에 추가 컨텍스트로 주입된다 +- [ ] hitl 도달 시 모든 lateral 시도 이력이 hitl_notes에 첨부된다 +- [ ] stagnation 이벤트가 transition_events에 기록된다 + +### Dependency Gate (#721) + +- [ ] queue dependency의 phase 확인은 DB 조회 기준이다 +- [ ] 재시작 후에도 dependency gate가 정확히 동작한다 +- [ ] dependency가 Failed/Hitl이면 blocked, DB에 없으면 open + +### Concurrency -- [ ] workspace.concurrency=2인 workspace에서 Running 아이템이 2개이면 추가 Ready→Running 전이가 발생하지 않는다 -- [ ] daemon.max_concurrent에 도달하면 모든 workspace에서 Ready→Running 전이가 중단된다 -- [ ] evaluate LLM 호출도 concurrency slot을 소비한다 +- [ ] workspace.concurrency + daemon.max_concurrent 2단계 제한 +- [ ] evaluate LLM 호출도 concurrency slot 소비 ### Graceful Shutdown -- [ ] SIGINT 수신 시 새 아이템 수집/전이를 즉시 중단한다 -- [ ] Running 아이템의 완료를 최대 30초 대기한다 -- [ ] 30초 초과 시 Running 아이템을 Pending으로 롤백하고 worktree를 보존한다 -- [ ] 재시작 후 롤백된 아이템이 기존 worktree를 재사용하여 Running에 재진입한다 +- [ ] SIGINT → 30초 대기 → Pending 롤백 + worktree 보존 ### 환경변수 -- [ ] handler/script 실행 시 WORK_ID, WORKTREE 두 환경변수만 주입된다 -- [ ] `belt context $WORK_ID --json`이 아이템의 전체 정보를 반환한다 +- [ ] handler/script에 WORK_ID, WORKTREE 2개만 주입 --- ### 관련 문서 -- [DESIGN-v5](../DESIGN-v5.md) — 설계 철학 +- [DESIGN-v6](../DESIGN-v6.md) — 전체 상태 흐름 + 설계 철학 +- [LifecycleHook](./lifecycle-hook.md) — 상태 전이 반응 trait - [QueuePhase 상태 머신](./queue-state-machine.md) — 상태 전이 상세 -- [DataSource](./datasource.md) — 워크플로우 정의 + context 스키마 +- [Stagnation Detection](./stagnation.md) — Composite Similarity + Lateral Thinking +- [DataSource](./datasource.md) — 수집/컨텍스트 추상화 - [AgentRuntime](./agent-runtime.md) — LLM 실행 추상화 - [Cron 엔진](./cron-engine.md) — evaluate cron + 품질 루프 diff --git a/spec/concerns/data-model.md b/spec/concerns/data-model.md index 2508675..eb34c1d 100644 --- a/spec/concerns/data-model.md +++ b/spec/concerns/data-model.md @@ -1,6 +1,6 @@ # Data Model -> 관련 문서: [DESIGN-v5](../DESIGN-v5.md), [QueuePhase 상태 머신](./queue-state-machine.md), [DataSource](./datasource.md), [Cron 엔진](./cron-engine.md) +> 관련 문서: [DESIGN-v6](../DESIGN-v6.md), [QueuePhase 상태 머신](./queue-state-machine.md), [DataSource](./datasource.md), [LifecycleHook](./lifecycle-hook.md), [Evaluator](./evaluator.md), [Cron 엔진](./cron-engine.md), [Stagnation](./stagnation.md) Belt의 모든 상태는 SQLite 단일 파일(`~/.belt/belt.db`)에 저장된다. 이 문서는 테이블 스키마, 도메인 모델, 직렬화 규칙을 한 곳에 정의한다. @@ -46,7 +46,7 @@ CREATE TABLE queue_items ( hitl_notes TEXT, -- 사용자 메모 hitl_reason TEXT, -- HitlReason enum (snake_case) hitl_timeout_at TEXT, -- 만료 시각 (RFC3339) - hitl_terminal_action TEXT, -- 만료 시 액션 ('skip' | 'replan') + hitl_terminal_action TEXT, -- EscalationAction enum (snake_case) ← v6: Option → Option -- 추적 필드 replan_count INTEGER NOT NULL DEFAULT 0, -- 재계획 횟수 (max 3) @@ -54,12 +54,14 @@ CREATE TABLE queue_items ( ); ``` +**v6 변경 (#720)**: `hitl_terminal_action`은 `EscalationAction` enum 값만 허용한다. DB에는 snake_case 문자열로 저장, 로드 시 `FromStr`로 파싱한다. 유효하지 않은 값은 파싱 에러. + **인메모리 전용 필드** (DB에 저장하지 않음): - `previous_worktree_path: Option` — retry 시 이전 아이템의 worktree 경로를 전달하기 위한 transient 필드 ### history -작업 시도 기록. append-only로만 쓰고, 읽기 전용으로 조회한다. `failure_count`는 이 테이블에서 계산한다. +작업 시도 기록. append-only로만 쓰고, 읽기 전용으로 조회한다. `failure_count`는 이 테이블에서 계산한다. **Stagnation detection도 이 테이블의 summary/error를 입력으로 사용한다.** ```sql CREATE TABLE history ( @@ -75,9 +77,23 @@ CREATE TABLE history ( ); ``` +**QueuePhase ↔ history.status 매핑**: + +| QueuePhase | history.status | 비고 | +|------------|---------------|------| +| Pending | — | 시도 아님, 기록 안 함 | +| Ready | — | 시도 아님, 기록 안 함 | +| Running | `running` | handler 실행 중 | +| Completed | — | 전이 상태, history에 기록 안 함 (`"completed"` 파싱 시 `Done`으로 매핑) | +| Done | `done` | 완료 | +| Hitl | `hitl` | 사람 대기 | +| Failed | `failed` | 실패 | +| Skipped | `skipped` | 건너뜀 | + **파생 쿼리**: - `failure_count`: `SELECT COUNT(*) FROM history WHERE source_id = ? AND state = ? AND status = 'failed'` - `max_attempt`: `SELECT MAX(attempt) FROM history WHERE source_id = ? AND state = ?` +- **stagnation 입력**: `SELECT summary, error FROM history WHERE source_id = ? AND state = ? ORDER BY attempt DESC LIMIT ?` ### transition_events @@ -88,7 +104,7 @@ CREATE TABLE transition_events ( id TEXT PRIMARY KEY, -- UUID work_id TEXT NOT NULL, source_id TEXT NOT NULL, - event_type TEXT NOT NULL, -- 'phase_enter' | 'handler' | 'evaluate' | 'on_done' | 'on_fail' + event_type TEXT NOT NULL, -- 'phase_enter' | 'handler' | 'evaluate' | 'hook' | 'stagnation' phase TEXT, -- 진입한 phase from_phase TEXT, -- 이전 phase detail TEXT, -- 사람이 읽을 수 있는 설명 @@ -96,10 +112,14 @@ CREATE TABLE transition_events ( ); ``` +**v6 변경**: `event_type`에 `'stagnation'`과 `'hook'` 추가. 기존 `'on_done'`/`'on_fail'`은 `'hook'`으로 통합 — LifecycleHook 트리거 이벤트를 기록한다. `detail`에 hook 종류(on_enter/on_done/on_fail/on_escalation), 탐지 패턴, confidence, evidence를 JSON으로 기록한다. + ### queue_dependencies 아이템 간 실행 순서 제약. `depends_on` 아이템이 Done이 아니면 `work_id` 아이템은 Ready→Running 전이가 블로킹된다. +**v6 변경 (#721)**: dependency phase 확인은 **DB 조회 기반**이다 (in-memory queue가 아님). 재시작 후에도 정확히 동작한다. + ```sql CREATE TABLE queue_dependencies ( work_id TEXT NOT NULL, @@ -224,11 +244,13 @@ CREATE TABLE knowledge_base ( | `Ready` | `"ready"` | No | 실행 준비 완료 (자동 전이) | | `Running` | `"running"` | No | worktree 생성 + handler 실행 중 | | `Completed` | `"completed"` | No | handler 성공, evaluate 대기 | -| `Done` | `"done"` | **Yes** | evaluate 완료 + on_done 성공 | +| `Done` | `"done"` | **Yes** | evaluate 완료 + hook.on_done() 성공 | | `Hitl` | `"hitl"` | No | 사람 판단 필요 | -| `Failed` | `"failed"` | No | on_done 실패 또는 인프라 오류 | +| `Failed` | `"failed"` | No | hook.on_done() 실패 또는 인프라 오류 | | `Skipped` | `"skipped"` | **Yes** | escalation skip 또는 preflight 실패 | +**v6 (#718)**: `phase` 필드는 `pub(crate)` 가시성. 외부에서 직접 대입 불가, 반드시 `QueueItem::transit()` 경유. 상세: [QueuePhase 상태 머신](./queue-state-machine.md) + ### SpecStatus 6개 상태. 직렬화 시 lowercase. @@ -255,18 +277,57 @@ HITL 생성 경로. 직렬화 시 snake_case. | `SpecConflict` | `"spec_conflict"` | 스펙 파일 겹침 | | `SpecCompletionReview` | `"spec_completion_review"` | 스펙 완료 최종 확인 | | `SpecModificationProposed` | `"spec_modification_proposed"` | Agent 수정 제안 | +| `StagnationDetected` | `"stagnation_detected"` | **v6** 반복 패턴 감지 + lateral thinking 사고 전환 | ### EscalationAction failure_count별 대응. 직렬화 시 snake_case. -| Variant | 직렬화 | on_fail 실행 | 설명 | -|---------|--------|:------------:|------| +| Variant | 직렬화 | hook.on_fail 트리거 | 설명 | +|---------|--------|:------------------:|------| | `Retry` | `"retry"` | **No** | 조용한 재시도 | -| `RetryWithComment` | `"retry_with_comment"` | Yes | on_fail + 재시도 | -| `Hitl` | `"hitl"` | Yes | on_fail + HITL 생성 | -| `Skip` | `"skip"` | Yes | on_fail + Skipped | -| `Replan` | `"replan"` | Yes | on_fail + HITL(replan) | +| `RetryWithComment` | `"retry_with_comment"` | Yes | hook.on_fail + 재시도 | +| `Hitl` | `"hitl"` | Yes | hook.on_fail + HITL 생성 | +| `Skip` | `"skip"` | Yes | hook.on_fail + Skipped | +| `Replan` | `"replan"` | Yes | hook.on_fail + HITL(replan) | + +모든 EscalationAction에서 `hook.on_escalation(action)` 트리거. `on_fail`은 Retry 제외 시 추가 트리거. + +**v6 (#720)**: `EscalationAction`은 `FromStr` + `Display` impl을 가진다. `hitl_terminal_action` 필드 타입으로도 사용된다. + +```rust +impl FromStr for EscalationAction { + type Err = BeltError; + fn from_str(s: &str) -> Result { /* snake_case 파싱 */ } +} +``` + +### StagnationPattern (v6 신규) + +정체 패턴 유형. 직렬화 시 snake_case. + +| Variant | 직렬화 | 설명 | +|---------|--------|------| +| `Spinning` | `"spinning"` | 동일/유사 출력 반복 (A→A→A) | +| `Oscillation` | `"oscillation"` | 교대 반복 (A→B→A→B) | +| `NoDrift` | `"no_drift"` | 진행 점수 정체 | +| `DiminishingReturns` | `"diminishing_returns"` | 개선폭 감소 | + +SPINNING/OSCILLATION은 `CompositeSimilarity`로 유사도 판단, NO_DRIFT/DIMINISHING은 drift score 수치 비교. + +### Persona (v6 신규) + +Lateral Thinking 사고 전환 페르소나. belt-core에 `include_str!`로 내장. 직렬화 시 snake_case. + +| Variant | 직렬화 | 패턴 친화도 | 전략 | +|---------|--------|-----------|------| +| `Hacker` | `"hacker"` | SPINNING | 제약 우회, 워크어라운드 | +| `Architect` | `"architect"` | OSCILLATION | 구조 재설계, 관점 전환 | +| `Researcher` | `"researcher"` | NO_DRIFT | 정보 수집, 체계적 디버깅 | +| `Simplifier` | `"simplifier"` | DIMINISHING | 복잡도 축소, 가정 제거 | +| `Contrarian` | `"contrarian"` | 복합/기타 | 가정 뒤집기, 문제 역전 | + +상세: [Stagnation Detection](./stagnation.md) ### HistoryStatus @@ -298,20 +359,23 @@ handlers: - script: "cargo test" ``` -### ScriptAction (yaml 설정) +### ScriptAction (yaml 설정, v6 호환용) -lifecycle hook(`on_enter`, `on_done`, `on_fail`)에서 사용. **script만 허용**. +v6 Phase 1에서 기존 yaml과의 호환을 위해 유지. `ScriptLifecycleHook` 어댑터가 이를 소비한다. Phase 2에서 DataSource별 Hook impl로 대체 예정. ```yaml +# v6 Phase 1: 기존 yaml 호환 (ScriptLifecycleHook 어댑터가 처리) on_done: - script: "gh pr create ..." on_fail: - script: "gh issue comment ..." ``` +> **v6 변경**: lifecycle 반응은 `LifecycleHook` trait으로 분리. yaml script는 어댑터를 통해 호환 유지. 상세: [LifecycleHook](./lifecycle-hook.md) + ### Action (런타임 추상화) -코어의 실행 단위. `HandlerConfig`와 `ScriptAction` 모두 `Action`으로 변환되어 실행된다. +코어의 실행 단위. handler의 `HandlerConfig`가 `Action`으로 변환되어 Executor가 실행한다. ``` HandlerConfig::Prompt → Action::Prompt { text, runtime, model } @@ -347,6 +411,29 @@ sources: 3: hitl terminal: skip # HITL 만료 시 ('skip' | 'replan') +# v6 신규: stagnation 탐지 + lateral thinking 설정 +stagnation: + enabled: true # default: true + spinning_threshold: 3 # default: 3 + oscillation_cycles: 2 # default: 2 + similarity_threshold: 0.8 # composite score 유사 판정 (default: 0.8) + no_drift_epsilon: 0.01 # default: 0.01 + no_drift_iterations: 3 # default: 3 + diminishing_threshold: 0.01 # default: 0.01 + confidence_threshold: 0.5 # default: 0.5 + + similarity: # CompositeSimilarity (Composite Pattern) + - judge: exact_hash # 기본 프리셋 + weight: 0.5 + - judge: token_fingerprint + weight: 0.3 + - judge: ncd + weight: 0.2 + + lateral: + enabled: true # default: true + max_attempts: 3 # 페르소나 최대 시도 (default: 3) + runtime: default: claude claude: @@ -368,6 +455,8 @@ runtime: `belt context $WORK_ID --json`이 반환하는 구조. script가 정보를 조회하는 유일한 방법. +**v6 변경 (#719)**: `source_data` 필드 추가. DataSource별 자유 스키마. 기존 `issue`/`pr` 필드는 호환성을 위해 유지. + ```json { "work_id": "github:org/repo#42:implement", @@ -382,6 +471,27 @@ runtime: "url": "https://github.com/org/repo", "default_branch": "main" }, + "source_data": { + "issue": { + "number": 42, + "title": "...", + "body": "...", + "labels": ["belt:implement"], + "author": "user", + "state": "open" + }, + "pr": { + "number": 43, + "title": "...", + "state": "open", + "draft": false, + "head_branch": "belt/42-implement", + "base_branch": "main", + "reviews": [ + { "reviewer": "user", "state": "APPROVED" } + ] + } + }, "issue": { "number": 42, "title": "...", @@ -414,6 +524,27 @@ runtime: } ``` +### source_data 마이그레이션 전략 (#719) + +| Phase | 상태 | 설명 | +|-------|------|------| +| **1 (v6)** | `source_data` + `issue`/`pr` 양쪽 채움 | 하위 호환. 기존 script 수정 불필요 | +| **2 (v7+)** | `issue`/`pr` deprecated | script가 `source_data` 경로로 전환 | +| **3 (v8+)** | `issue`/`pr` 제거 | `source_data`만 사용 | + +script에서의 접근: +```bash +# v6 (양쪽 모두 가능) +belt context $WORK_ID --json | jq '.issue.number' +belt context $WORK_ID --json | jq '.source_data.issue.number' + +# v7+ (source_data 권장) +belt context $WORK_ID --json | jq '.source_data.issue.number' + +# Jira DataSource (v7+) +belt context $WORK_ID --json | jq '.source_data.ticket.key' +``` + --- ## 타임스탬프 규칙 diff --git a/spec/concerns/datasource.md b/spec/concerns/datasource.md index 38b7cbc..c05d0e8 100644 --- a/spec/concerns/datasource.md +++ b/spec/concerns/datasource.md @@ -8,17 +8,19 @@ ## 역할 ``` -DataSource가 소유하는 것: - 1. 수집 — 어떤 조건에서 아이템을 감지하는가 (trigger) - 2. 컨텍스트 — 해당 아이템의 외부 시스템 정보를 어떻게 조회하는가 (context) +DataSource가 소유하는 것 (읽기): + 1. 수집 — 어떤 조건에서 아이템을 감지하는가 (collect) + 2. 컨텍스트 — 해당 아이템의 외부 시스템 정보를 어떻게 조회하는가 (get_context) -코어/yaml이 소유하는 것: - 3. 처리 — 감지된 아이템을 어떻게 처리하는가 (handlers: prompt/script) - 4. 전이 — 처리 완료 후 다음에 뭘 트리거하는가 (on_done script) - 5. 실패 반영 — 실패 시 외부 시스템에 어떻게 알리는가 (on_fail script) - 6. 실패 정책 — 실패 시 어떻게 escalation하는가 (escalation) +LifecycleHook이 소유하는 것 (쓰기/반응): + 3. 상태 반응 — 상태 전이 시 외부 시스템에 어떻게 반영하는가 (on_enter/on_done/on_fail/on_escalation) -코어는 DataSource 내부를 모른다. collect() 결과를 큐에 넣고, 상태 전이만 관리. +yaml이 소유하는 것: + 4. 처리 — 감지된 아이템을 어떻게 처리하는가 (handlers: prompt/script) + 5. 실패 정책 — 실패 시 어떻게 escalation하는가 (escalation) + +코어는 DataSource/LifecycleHook 내부를 모른다. collect() 결과를 큐에 넣고, 상태 전이 시 hook을 트리거할 뿐. +상세: [LifecycleHook](./lifecycle-hook.md) ``` --- @@ -40,9 +42,11 @@ pub trait DataSource: Send + Sync { ``` v4 대비 대폭 축소. `on_phase_enter`, `on_failed`, `on_done`, `before_task`, `after_task` 모두 제거. -- on_done/on_fail → yaml에 정의된 script가 처리 (gh CLI 등 직접 호출) +- on_done/on_fail/on_enter/on_escalation → `LifecycleHook` trait으로 분리. 상세: [LifecycleHook](./lifecycle-hook.md) - worktree 셋업 → 인프라 레이어가 항상 처리 -- escalation → yaml의 escalation 정책을 코어가 실행 +- escalation → yaml의 escalation 정책을 코어가 결정, hook이 반응 + +**v6 (#719)**: `get_context()`가 반환하는 `ItemContext`에 `source_data: serde_json::Value` 필드가 추가된다. DataSource는 자신의 고유 데이터를 `source_data`에 자유 스키마로 채운다. --- @@ -67,6 +71,8 @@ Daemon이 주입하는 환경변수는 **2개만**: ### GitHub context 스키마 +**v6 (#719)**: `source_data` 필드에 DataSource 고유 데이터를 담는다. 기존 `issue`/`pr` 필드는 Phase 1에서 호환성을 위해 유지. + ```json { "work_id": "github:org/repo#42:implement", @@ -81,6 +87,20 @@ Daemon이 주입하는 환경변수는 **2개만**: "url": "https://github.com/org/repo", "default_branch": "main" }, + "source_data": { + "issue": { + "number": 42, + "title": "JWT middleware 구현", + "body": "...", + "labels": ["belt:implement"], + "author": "irene" + }, + "pr": { + "number": 87, + "head_branch": "feat/jwt-middleware", + "review_comments": [] + } + }, "issue": { "number": 42, "title": "JWT middleware 구현", @@ -91,7 +111,7 @@ Daemon이 주입하는 환경변수는 **2개만**: "pr": { "number": 87, "head_branch": "feat/jwt-middleware", - "review_comments": [...] + "review_comments": [] }, "history": [ { "state": "analyze", "status": "done", "attempt": 1, "summary": "구현 가능" }, @@ -102,6 +122,8 @@ Daemon이 주입하는 환경변수는 **2개만**: } ``` +> **source_data 마이그레이션**: Phase 1(v6)에서는 `issue`/`pr`과 `source_data` 양쪽 모두 채운다. Phase 2(v7+)에서 기존 필드 deprecated, Phase 3(v8+)에서 제거. 상세: [Data Model](./data-model.md#source_data-마이그레이션-전략-719) + ### history는 append-only 같은 `source_id`의 모든 이벤트가 시간순으로 축적된다. 실패 횟수는 history에서 계산: @@ -113,7 +135,7 @@ FAILURES=$(echo $CTX | jq '[.history[] | select(.status=="failed" and .state=="i 별도 `failure_count` 컬럼 없이 history 조회만으로 충분. -### Jira context 스키마 (v6+) +### Jira context 스키마 (v7+) ```json { @@ -128,21 +150,25 @@ FAILURES=$(echo $CTX | jq '[.history[] | select(.status=="failed" and .state=="i "type": "jira", "url": "https://jira.company.com/project/BE" }, - "ticket": { - "key": "BE-123", - "summary": "...", - "status": "In Progress", - "assignee": "irene" + "source_data": { + "ticket": { + "key": "BE-123", + "summary": "...", + "status": "In Progress", + "assignee": "irene" + } }, - "history": [...] + "history": [] } ``` +> Jira DataSource는 `source_data.ticket`에 데이터를 채운다. `issue`/`pr` 필드는 없음 — `source_data`만으로 OCP 달성. + --- ## 상태 기반 워크플로우 -각 DataSource는 자기 시스템의 상태 표현으로 워크플로우를 정의한다. v5는 GitHub에 집중한다. +각 DataSource는 자기 시스템의 상태 표현으로 워크플로우를 정의한다. v6는 GitHub에 집중한다. ### GitHub (라벨 기반) @@ -160,7 +186,7 @@ sources: on_done: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.issue.number') + ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') REPO=$(echo $CTX | jq -r '.source.url') gh issue edit $ISSUE --remove-label "belt:analyze" -R $REPO gh issue edit $ISSUE --add-label "belt:implement" -R $REPO @@ -172,9 +198,9 @@ sources: on_done: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.issue.number') + ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') REPO=$(echo $CTX | jq -r '.source.url') - TITLE=$(echo $CTX | jq -r '.issue.title') + TITLE=$(echo $CTX | jq -r '.source_data.issue.title // .issue.title') gh pr create --title "$TITLE" --body "Closes #$ISSUE" -R $REPO gh issue edit $ISSUE --remove-label "belt:implement" -R $REPO gh issue edit $ISSUE --add-label "belt:review" -R $REPO @@ -186,7 +212,7 @@ sources: on_done: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.issue.number') + ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') REPO=$(echo $CTX | jq -r '.source.url') gh issue edit $ISSUE --remove-label "belt:review" -R $REPO gh issue edit $ISSUE --add-label "belt:done" -R $REPO @@ -198,15 +224,15 @@ sources: terminal: skip # hitl timeout 시 적용 (skip 또는 replan) ``` -### 향후 확장 (v6+) +### 향후 확장 (v7+) -DataSource trait을 구현하면 코어 변경 없이 새 외부 시스템을 추가할 수 있다. +DataSource trait을 구현하면 코어 변경 없이 새 외부 시스템을 추가할 수 있다. `source_data`를 통해 코어 타입 변경도 불필요. -| 시스템 | 상태 표현 | trigger 예시 | -|--------|----------|-------------| -| Jira | 티켓 status | `{ status: "To Analyze" }` | -| Slack | 리액션 | `{ reaction: "robot_face" }` | -| Linear | 라벨/status | `{ label: "belt" }` | +| 시스템 | 상태 표현 | trigger 예시 | source_data | +|--------|----------|-------------|-------------| +| Jira | 티켓 status | `{ status: "To Analyze" }` | `source_data.ticket` | +| Slack | 리액션 | `{ reaction: "robot_face" }` | `source_data.message` | +| Linear | 라벨/status | `{ label: "belt" }` | `source_data.issue` | --- @@ -227,36 +253,18 @@ handler 배열은 Running 상태에서 순차 실행. 하나라도 실패 시 on --- -## on_done / on_fail / on_enter +## Lifecycle Hook — LifecycleHook trait으로 분리 -모든 lifecycle hook은 **script 배열**로 정의. handler와 동일한 통합 액션 타입. +v6에서 on_done/on_fail/on_enter/on_escalation은 `LifecycleHook` trait으로 분리된다. Daemon은 상태 전이 시 hook을 트리거만 하고, 실행 책임은 Hook impl이 가진다. -```yaml -states: - implement: - trigger: { label: "belt:implement" } - on_enter: # Running 진입 시 (선택) - - script: | - CTX=$(belt context $WORK_ID --json) - echo "시작: $(echo $CTX | jq -r '.issue.title')" - handlers: - - prompt: "이슈를 구현해줘" - on_done: # 성공 시 - - script: | - CTX=$(belt context $WORK_ID --json) - # PR 생성, 라벨 전환 등 - on_fail: # 실패 시 (escalation 전) - - script: | - CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.issue.number') - REPO=$(echo $CTX | jq -r '.source.url') - gh issue comment $ISSUE --body "구현 실패" -R $REPO -``` +상세: [LifecycleHook](./lifecycle-hook.md) -실행 주체: Daemon이 상태 전이 시점에 직접 실행. -- `on_enter`: Running 진입 후, handler 실행 전. **실패 시 handler를 실행하지 않고 즉시 escalation 정책을 적용**한다 (handler 실패와 동일한 경로). on_enter 실패도 history에 failed 이벤트로 기록되며 failure_count에 포함된다. -- `on_done`: evaluate가 Done 판정 후 (script 실패 시 → Failed 상태) -- `on_fail`: handler 또는 on_enter 실패 시, escalation level에 따라 조건부 실행 (`retry`에서는 실행 안 함) +| hook | 트리거 시점 | 실패 시 | +|------|-----------|--------| +| `on_enter` | Running 진입 후, handler 실행 전 | handler 건너뛰고 escalation | +| `on_done` | evaluate가 Done 판정 후 | Failed 상태로 전이 | +| `on_fail` | handler/on_enter 실패 시 (retry 제외) | — | +| `on_escalation` | escalation 결정 후 | — | --- @@ -278,12 +286,7 @@ escalation: # 선택지: skip (종료) 또는 replan (스펙 수정 제안) ``` -> **설계 의도**: level 4(skip)와 level 5(replan)는 순차적으로 도달할 수 없다. skip은 terminal 상태(Skipped)이므로 이후 실패가 발생하지 않는다. 따라서 skip과 replan은 level 3(hitl) 이후의 **대안적 선택지**로 재설계하였다. -> -> - `terminal: skip` — hitl timeout 시 해당 아이템을 건너뛰고 종료 -> - `terminal: replan` — hitl timeout 시 스펙 수정을 제안하는 HITL(replan) 이벤트 생성 -> -> 사람이 hitl에 직접 응답하는 경우, done/retry/skip/replan 중 자유롭게 선택할 수 있다. +> **v6 Stagnation과 Escalation의 관계**: escalation은 failure_count 기반으로 결정되고, stagnation은 lateral_plan 주입에 집중한다. 두 관심사는 직교한다 — escalation이 "언제 멈출지"를 결정하고, stagnation이 "다르게 시도할지"를 결정한다. escalation 발생 시 `LifecycleHook.on_escalation()`이 DataSource별 반응을 처리한다. 상세: [LifecycleHook](./lifecycle-hook.md) ### on_fail 실행 조건 @@ -325,7 +328,10 @@ queue_items 테이블: ### 관련 문서 -- [DESIGN-v5](../DESIGN-v5.md) — 전체 아키텍처 +- [DESIGN-v6](../DESIGN-v6.md) — 전체 아키텍처 +- [LifecycleHook](./lifecycle-hook.md) — 상태 전이 반응 trait - [AgentRuntime](./agent-runtime.md) — handler prompt 실행 +- [Stagnation Detection](./stagnation.md) — 실패 패턴 감지 - [Cron 엔진](./cron-engine.md) — 품질 루프 - [CLI 레퍼런스](./cli-reference.md) — belt context CLI +- [Data Model](./data-model.md) — source_data 마이그레이션 전략 diff --git a/spec/draft/concerns/evaluator.md b/spec/concerns/evaluator.md similarity index 100% rename from spec/draft/concerns/evaluator.md rename to spec/concerns/evaluator.md diff --git a/spec/draft/concerns/lifecycle-hook.md b/spec/concerns/lifecycle-hook.md similarity index 100% rename from spec/draft/concerns/lifecycle-hook.md rename to spec/concerns/lifecycle-hook.md diff --git a/spec/concerns/queue-state-machine.md b/spec/concerns/queue-state-machine.md index 2587622..e1ddade 100644 --- a/spec/concerns/queue-state-machine.md +++ b/spec/concerns/queue-state-machine.md @@ -1,7 +1,7 @@ # QueuePhase 상태 머신 > 큐 아이템의 전체 생명주기를 정의한다. -> 상위 설계는 [DESIGN-v5](../DESIGN-v5.md) 참조. +> 상위 설계는 [DESIGN-v6](../DESIGN-v6.md) 참조. --- @@ -20,6 +20,46 @@ --- +## Phase 전이 캡슐화 (v6 #718) + +`QueueItem.phase` 필드를 직접 대입하면 `can_transition_to()` 검증을 우회할 수 있다. +v6에서는 모든 전이를 `QueueItem::transit()` 메서드로 강제한다. + +```rust +impl QueueItem { + /// phase 필드는 pub(crate) — belt-core 외부에서 직접 대입 불가 + /// 읽기는 pub getter: fn phase(&self) -> QueuePhase + + pub fn transit(&mut self, to: QueuePhase) -> Result { + let from = self.phase; + if !from.can_transition_to(&to) { + return Err(BeltError::InvalidTransition { from, to }); + } + self.phase = to; + self.updated_at = Utc::now().to_rfc3339(); + Ok(from) + } +} +``` + +### 테스트 지원 + +테스트에서 특정 phase의 아이템을 생성하려면 빌더를 사용한다: + +```rust +// 테스트 전용 빌더 (cfg(test) 또는 #[doc(hidden)]) +QueueItem::builder() + .work_id("test:1:analyze") + .with_phase(QueuePhase::Running) // 검증 없이 직접 설정 + .build() +``` + +### DB 로드 + +`belt-infra/db.rs`의 `from_row()`는 `pub(crate)` 접근 가능하므로 DB에서 로드 시 phase 직접 설정이 가능하다. + +--- + ## 전체 상태 전이 ``` @@ -52,20 +92,30 @@ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────────────────┐ - │ Completed │ │ Escalation 정책 적용 │ - │ │ │ (history 기반 count) │ - │ handler 완료 │ │ │ - │ evaluate 대기 │ │ 1: retry │ - │ │ │ → 새 아이템 → Pending │ - │ force_trigger │ │ → worktree 보존 │ - │ ("evaluate") │ │ → on_fail 실행 안 함 │ - └────────┬────────┘ │ │ + │ Completed │ │ Stagnation Analyzer (항상 실행)│ + │ │ │ │ + │ handler 완료 │ │ ① CompositeSimilarity로 │ + │ evaluate 대기 │ │ outputs/errors 유사도 분석 │ + │ │ │ ② 패턴 감지 시 │ + │ force_trigger │ │ LateralAnalyzer가 │ + │ ("evaluate") │ │ 내장 페르소나로 대안 분석 │ + └────────┬────────┘ │ → lateral_plan 생성 │ + │ │ │ + │ │ Escalation (failure_count): │ + │ │ 1: retry │ + │ │ → lateral_plan 주입 │ + │ │ → 새 아이템 → Pending │ + │ │ → worktree 보존 │ + │ │ → on_fail 실행 안 함 │ + │ │ │ │ │ 2: retry_with_comment │ + │ │ → lateral_plan 주입 │ │ │ → on_fail script 실행 │ │ │ → 새 아이템 → Pending │ │ │ → worktree 보존 │ │ │ │ │ │ 3: hitl │ + │ │ → lateral_report 첨부 │ │ │ → on_fail script 실행 │ │ │ → HITL 이벤트 생성 ───────┐│ │ │ → worktree 보존 ││ @@ -76,7 +126,7 @@ │ │ replan → HITL(replan) ────┤│ │ │ └───────────────────────────────┘│ │ │ │ │ - │ evaluate cron │ │ + │ evaluate cron (per-item) │ │ │ (LLM이 belt queue done/hitl CLI 호출) │ │ │ │ │ ┌────┴────┐ │ │ @@ -139,6 +189,8 @@ failure_count는 append-only history에서 계산한다: `history | filter(state, failed) | count`. on_enter 실패도 handler 실패와 동일하게 failure_count에 포함된다. +> **v6 (#723)**: 모든 실패에서 StagnationDetector가 CompositeSimilarity로 유사도 분석을 수행한다. 패턴이 감지되면 LateralAnalyzer가 내장 페르소나(HACKER, ARCHITECT 등)로 대안 접근법을 분석하고, lateral_plan을 생성하여 retry 시 handler prompt에 주입한다. escalation 자체는 기존 failure_count 기반 그대로이되, **모든 retry가 lateral plan으로 강화**된다. 상세: [Stagnation Detection](./stagnation.md) + --- ## Evaluate 원칙 @@ -149,7 +201,23 @@ failure_count는 append-only history에서 계산한다: `history | filter(state 2. **"충분한가?"만 판단** — "이 handler의 결과물이 다음 단계로 넘어가기에 충분한가?"만 본다. 품질 판단(좋은 코드인가?)은 Cron 품질 루프가 담당한다. -3. **state별 구체 기준은 agent-workspace rules에 위임** — `~/.belt/agent-workspace/.claude/rules/classify-policy.md`에 state별 Done 조건을 정의한다 (Agent 워크스페이스의 rules 파일, [Agent 워크스페이스](./agent-workspace.md) 참조). 코어는 rules를 모르고, `belt agent`가 rules를 참조하여 판단한다. +3. **state별 구체 기준은 agent-workspace rules에 위임** — `~/.belt/agent-workspace/.claude/rules/classify-policy.md`에 state별 Done 조건을 정의한다. 코어는 rules를 모르고, `belt agent`가 rules를 참조하여 판단한다. + +### Per-Item 판정 (v6 #722) + +evaluate는 **per-work_id 단위**로 LLM 판정을 실행한다. 각 Completed 아이템에 대해 개별 프롬프트를 발행하고, 해당 아이템의 context를 포함한다. + +``` +for item in queue.get(Completed): + belt_agent_p(workspace, + "아이템 {work_id}의 완료 여부를 판단해줘. + belt context {work_id} --json 으로 컨텍스트를 확인하고, + belt queue done {work_id} 또는 belt queue hitl {work_id} 를 실행해줘") +``` + +- 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템 판정에 영향 없다 +- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비한다 — 별도 batch 제어 없음 +- 기존 `eval_failure_counts`는 이미 per-work_id로 관리됨 (설계 의도 일치) ### 실패 원칙 @@ -164,10 +232,15 @@ Completed는 **안전한 대기 상태**. evaluate가 실패하든 CLI가 실패 --- ---- - ## 수용 기준 +### Phase 전이 캡슐화 (#718) + +- [ ] `QueueItem.phase` 필드는 `pub(crate)` 가시성으로, belt-core 외부에서 직접 대입 불가 +- [ ] 모든 phase 변경은 `QueueItem::transit(to)` 메서드를 경유한다 +- [ ] `transit()` 메서드는 내부에서 `can_transition_to()` 검증 + `updated_at` 갱신을 수행한다 +- [ ] 테스트 코드에서도 phase 직접 대입 대신 `transit()` 또는 테스트 헬퍼를 사용한다 + ### 상태 전이 규칙 - [ ] Pending→Ready 전이는 Daemon tick마다 자동 수행된다 @@ -182,9 +255,13 @@ Completed는 **안전한 대기 상태**. evaluate가 실패하든 CLI가 실패 - [ ] failure_count=2일 때 `retry_with_comment`가 적용되면 on_fail 실행 후 새 아이템으로 재시도한다 - [ ] failure_count=3일 때 `hitl`이 적용되면 on_fail 실행 후 HITL 이벤트가 생성된다 - [ ] on_enter 실패도 failure_count에 포함된다 +- [ ] 모든 실패에서 stagnation 분석이 실행되고, 패턴 감지 시 lateral_plan이 retry에 주입된다 -### Evaluate 실패 시 재시도 +### Evaluate (per-item, #722) +- [ ] evaluate는 per-work_id 단위로 LLM 판정을 실행한다 +- [ ] 각 판정에 해당 아이템의 context가 포함된다 +- [ ] 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템에 영향 없다 - [ ] evaluate LLM 오류 시 아이템은 Completed에 머무르고, 다음 cron tick에서 재시도된다 - [ ] evaluate 반복 실패(N회)로 HITL 에스컬레이션 시 HitlReason::EvaluateFailure가 기록된다 - [ ] on_done script 실패 시 Failed 전이되고, on_fail은 실행하지 않는다 @@ -200,8 +277,11 @@ Completed는 **안전한 대기 상태**. evaluate가 실패하든 CLI가 실패 ### 관련 문서 -- [DESIGN-v5](../DESIGN-v5.md) — 설계 철학 -- [DataSource](./datasource.md) — escalation 정책 + on_fail script +- [DESIGN-v6](../DESIGN-v6.md) — 설계 철학 +- [Daemon](./daemon.md) — 내부 모듈 구조 + 실행 루프 +- [Stagnation Detection](./stagnation.md) — 반복 패턴 감지 + lateral thinking +- [LifecycleHook](./lifecycle-hook.md) — 상태 전이 반응 trait +- [DataSource](./datasource.md) — 수집/컨텍스트 + escalation 정책 - [Cron 엔진](./cron-engine.md) — evaluate cron + force_trigger - [실패 복구와 HITL](../flows/04-failure-and-hitl.md) — 실패/HITL 시나리오 - [Data Model](./data-model.md) — 테이블 스키마, 도메인 enum diff --git a/spec/draft/concerns/stagnation.md b/spec/concerns/stagnation.md similarity index 100% rename from spec/draft/concerns/stagnation.md rename to spec/concerns/stagnation.md diff --git a/spec/draft/.gitkeep b/spec/draft/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/spec/draft/README.md b/spec/draft/README.md deleted file mode 100644 index c94c5b5..0000000 --- a/spec/draft/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Spec v6 Draft - -> **Date**: 2026-04-04 -> **Status**: Draft -> **구조**: 설계 개요 + 관심사별 상세 스펙 + 사용자 플로우 - -## 핵심 변경 (v5 → v6) - -- **LifecycleHook 분리**: on_done/on_fail/on_enter를 yaml script에서 `LifecycleHook` trait으로 분리. handler(작업)와 hook(반응) 관심사 분리. DataSource별 impl, workspace별 바인딩, lazy 로딩 -- **Daemon = CPU**: yaml script 실행기 → 상태 머신 CPU. hook 트리거만, 실행 책임은 Hook impl이 소유 -- **Stagnation Detection**: 실패 횟수가 아니라 실패 패턴(SPINNING, OSCILLATION)을 감지 -- **Daemon 모듈 분리**: 단일 daemon.rs → Advancer + Executor + HitlService + StagnationDetector 모듈 -- **Phase 전이 캡슐화**: `item.phase` 직접 대입 금지, `QueueItem::transit()` 메서드 강제 -- **ItemContext 확장**: `source_data: serde_json::Value` 추가 — 새 DataSource 추가 시 코어 변경 0 -- **hitl_terminal_action 타입 안전**: `Option` → `Option` -- **Dependency Gate DB 기반**: in-memory → DB 조회, 재시작 시 순서 보장 -- **Evaluator per-item 판정**: workspace 배치 → per-work_id 개별 LLM 판정 - -## 설계 문서 - -- **[DESIGN-v6.md](./DESIGN-v6.md)** — 설계 철학 + 전체 구조 개요 (간결) - -## 관심사별 상세 스펙 (concerns/) - -"이 시스템은 내부적으로 어떻게 동작하지?" — 구현자 대상 - -| 문서 | 설명 | -|------|------| -| [QueuePhase 상태 머신](./concerns/queue-state-machine.md) | 8개 phase 전이, **전이 캡슐화**, worktree 생명주기, on_fail 조건 | -| [Daemon](./concerns/daemon.md) | **내부 모듈 구조**, 실행 루프, **DB dependency gate**, concurrency, graceful shutdown | -| [Evaluator](./concerns/evaluator.md) | **v6 신규** — Progressive Evaluation Pipeline, Evaluate before Execute | -| [Stagnation Detection](./concerns/stagnation.md) | **v6 신규** — 4가지 정체 패턴, 해시 기반 탐지 | -| [LifecycleHook](./concerns/lifecycle-hook.md) | **v6 신규** — 상태 전이 반응 trait, handler/hook 분리, lazy 로딩 | -| [DataSource](./concerns/datasource.md) | 외부 시스템 추상화 trait + **source_data** + 워크플로우 yaml | -| [AgentRuntime](./concerns/agent-runtime.md) | LLM 실행 추상화 trait + Registry | -| [Agent 워크스페이스](./concerns/agent-workspace.md) | 대화형 에이전트 + **per-item evaluate** + slash command | -| [Cron 엔진](./concerns/cron-engine.md) | 주기 실행 + 품질 루프 (evaluate는 Daemon tick으로 이동) | -| [CLI 레퍼런스](./concerns/cli-reference.md) | 3-layer SSOT + `belt context` + 전체 커맨드 트리 | -| [Cross-Platform](./concerns/cross-platform.md) | OS 추상화 (ShellExecutor, DaemonNotifier) | -| [Distribution](./concerns/distribution.md) | 배포 전략 | -| [Data Model](./concerns/data-model.md) | SQLite 스키마, **StagnationPattern enum**, **EscalationAction FromStr**, **source_data** | - -## 사용자 플로우 (flows/) - -"사용자가 X를 하면 어떻게 되지?" — 시나리오 기반, 기획자/사용자 대상 - -| # | Flow | 설명 | -|---|------|------| -| 01 | [온보딩](./flows/01-setup.md) | workspace 등록 → 컨벤션 부트스트랩 | -| 02 | [스펙 생명주기](./flows/02-spec-lifecycle.md) | 스펙 등록 → 이슈 분해 → 완료 판정 | -| 03 | [이슈 파이프라인](./flows/03-issue-pipeline.md) | handlers 실행 → **stagnation detection** → evaluate → hook.on_done | -| 04 | [실패 복구와 HITL](./flows/04-failure-and-hitl.md) | **stagnation + lateral thinking** → escalation → hook 트리거 → 사람 개입 | -| 05 | [모니터링](./flows/05-monitoring.md) | TUI + CLI + /agent 시각화 + **stagnation 표시** | - -## 이슈 매핑 - -| 이슈 | 주요 반영 문서 | -|------|-------------| -| 신규 LifecycleHook 분리 | lifecycle-hook.md, datasource.md, daemon.md, DESIGN | -| #723 Stagnation/Oscillation 탐지 | stagnation.md, daemon.md, data-model.md, flow-04, DESIGN | -| #717 Daemon 내부 모듈 분리 | daemon.md, DESIGN | -| #718 Phase 전이 캡슐화 | queue-state-machine.md, data-model.md, DESIGN | -| #719 ItemContext source_data | data-model.md, datasource.md | -| #720 hitl_terminal_action 타입 | data-model.md, flow-04 | -| #721 Dependency Gate DB 기반 | daemon.md | -| #722 Evaluator per-item 판정 | cron-engine.md, agent-workspace.md, daemon.md | diff --git a/spec/draft/concerns/daemon.md b/spec/draft/concerns/daemon.md deleted file mode 100644 index 3833678..0000000 --- a/spec/draft/concerns/daemon.md +++ /dev/null @@ -1,355 +0,0 @@ -# Daemon — State Machine CPU - -> Daemon은 상태 머신을 틱마다 순회하며 전이를 결정하고, hook을 트리거하는 CPU. -> handler(prompt/script)를 실행하고, 상태 전이 시 workspace의 LifecycleHook을 트리거한다. -> GitHub 라벨, PR 생성 같은 도메인 로직을 모른다 — hook.on_*()의 Result만 받을 뿐. -> 내부는 Advancer·Executor·HitlService 모듈로 분리. 실패 시 StagnationDetector + LateralAnalyzer가 사고를 전환하여 재시도한다. - ---- - -## 역할 - -``` -1. 수집: DataSource.collect() → Pending에 넣기 -2. 전이: Pending → Ready → Running (자동, concurrency 제한) -3. 트리거: Running 진입 시 hook.on_enter() 트리거 -4. 실행: yaml에 정의된 handler(prompt/script) 실행 -5. 완료: handler 성공 → Completed 전이 -6. 분류: evaluate가 Completed → Done or HITL 판정 (per-item) -7. 반응: 상태 전이 시 hook.on_done/on_fail/on_escalation 트리거 -8. 스케줄: Cron engine으로 주기 작업 실행 - -Daemon이 아는 것: 상태 머신 + 언제 어떤 hook을 트리거할지 -Daemon이 모르는 것: hook이 실제로 무엇을 하는지 (Result만 받음) -``` - ---- - -## 내부 모듈 구조 (#717) - -Daemon은 상태 머신을 순회하며 전이를 결정하고 hook을 트리거하는 CPU이다. - -``` -Daemon (CPU) - loop { - collector.collect() - advancer.advance() - executor.execute() // handler 실행 + hook 트리거 - cron_engine.tick() - } -``` - -| 모듈 | 책임 | 소유하는 상태 | -|------|------|-------------| -| **Advancer** | Pending→Ready→Running 전이, dependency gate (DB), conflict 검출 | queue, ConcurrencyTracker | -| **Executor** | handler 실행 + hook 트리거, 실패 시 stagnation 분석 + lateral plan + escalation | ActionExecutor, StagnationDetector, LateralAnalyzer | -| **Evaluator** | Completed → Done/HITL 분류 (per-item, 이미 분리됨) | eval_failure_counts | -| **HitlService** | HITL 응답 처리, timeout 만료, terminal action 적용 | — (DB 직접 조회) | -| **CronEngine** | cron tick, force_trigger (이미 분리됨) | CronJob 목록 | - -### Executor 내부 구조 - -``` -Executor - │ - ├── ActionExecutor handler(prompt/script) 실행 - │ - ├── hook: &dyn LifecycleHook 상태 전이 시 트리거 (실행 책임은 Hook impl) - │ ├── on_enter() - │ ├── on_done() - │ ├── on_fail() - │ └── on_escalation() - │ - ├── StagnationDetector 실패 시 패턴 탐지 - │ └── judge: Box - │ └── CompositeSimilarity - │ ├── ExactHash (w: 0.5) - │ ├── TokenFingerprint (w: 0.3) - │ └── NCD (w: 0.2) - │ - └── LateralAnalyzer 패턴 감지 시 사고 전환 - └── personas/ (include_str! 내장) - hacker.md, architect.md, researcher.md, - simplifier.md, contrarian.md -``` - -### 모듈 간 의존 - -``` -Daemon - ├── Advancer (queue, db, dependency_guard) - ├── Executor (action_executor, stagnation_detector, lateral_analyzer) - ├── Evaluator (workspace_config) - ├── HitlService (db) - └── CronEngine (db) -``` - -- 모듈 간 의존은 trait 또는 함수 파라미터로만 전달 (순환 참조 금지) -- 각 모듈은 독립적으로 단위 테스트 가능 -- StagnationDetector는 `Box` 하나만 의존 (Composite 또는 단일) - ---- - -## Concurrency 제어 - -두 레벨로 동시 실행을 제어한다: - -```yaml -# workspace.yaml — workspace 루트 레벨에 정의 -concurrency: 2 # 이 workspace에서 동시 Running 아이템 수 - -# daemon 글로벌 설정 (별도 config) — 전체 workspace 합산 상한 -max_concurrent: 4 -``` - -- **workspace.concurrency**: workspace yaml 루트에 정의. "이 프로젝트에 동시에 몇 개까지 돌릴까". 모든 source의 아이템 합산 기준. -- **daemon.max_concurrent**: "머신 리소스 한계" (evaluate cron의 LLM 호출도 slot을 소비) - -Advancer는 `Ready → Running` 전이 시 두 제한을 모두 확인한다. - ---- - -## 실행 루프 (의사코드) - -``` -loop { - // 1. 수집 - for binding in workspace_bindings: - for source in binding.sources: - items = source.collect() - queue.push(Pending, items) - - // 2. 판정 (Evaluator) — 실행보다 먼저 - // Completed 아이템을 비용 순으로 판정: Mechanical → Semantic → (Consensus) - // Ready 아이템 중 이전 기록으로 판정 가능한 것은 handler 실행 없이 판정 - evaluator.evaluate() - - // 3. 자동 전이 (Advancer) - advancer.advance_pending_to_ready() // spec dep gate (DB) - advancer.advance_ready_to_running(limit) // queue dep gate (DB) + concurrency - - // 4. 실행 (Executor) - for item in queue.get_new(Running): - binding = lookup_workspace_binding(item) - hook = binding.hook // 이 workspace의 LifecycleHook - state = lookup_state(item) - worktree = create_or_reuse_worktree(item) - ctx = build_hook_context(item, worktree) - - // on_enter hook 트리거 (실패 시 handler 건너뛰고 실패 경로) - result = hook.on_enter(&ctx) - if result.failed: - executor.handle_failure(item, hook) - continue - - // handlers 순차 실행 (lateral_plan 있으면 prompt에 주입) - for action in state.handlers: - result = executor.execute(action, WORK_ID=item.id, WORKTREE=worktree, - lateral_plan=item.lateral_plan) - if result.failed: - executor.handle_failure(item, hook) - break - else: - item.transit(Completed) - - // 5. cron tick (품질 루프: gap-detection, knowledge-extract 등) - cron_engine.tick() -} -``` - -### Executor.handle_failure — Stagnation + Lateral + Hook 트리거 - -``` -fn handle_failure(item, hook): - // ① Stagnation Detection (항상 실행) - // 각 PatternDetector가 DB에서 자기 관심사 데이터를 직접 조회 - detections = stagnation_detector.detect(item.source_id, item.state, db) - active = detections.filter(|d| d.detected && d.confidence >= threshold) - - // ② Lateral Plan 생성 (패턴 감지 시) - lateral_plan = None - if active.is_not_empty() && lateral_config.enabled: - tried = db.get_tried_personas(item.source_id, item.state) - persona = select_persona(active[0].pattern, tried) - if persona.is_some(): - lateral_plan = lateral_analyzer.analyze( - detection=active[0], - persona=persona, - workspace=item.workspace_id, - ) - - // ③ transition_events에 stagnation 기록 - record_stagnation_event(item, detections, lateral_plan) - - // ④ Escalation 결정 (failure_count 기반) - failure_count = count_failures(item.source_id, item.state) - escalation = lookup_escalation(failure_count) - - // ⑤ Hook 트리거 — Daemon은 트리거만, 실행 책임은 Hook impl - ctx = build_hook_context(item, worktree) - hook.on_escalation(&ctx, escalation) - - if escalation.should_run_on_fail(): - hook.on_fail(&ctx) - - // ⑥ 상태 전이 - match escalation: - retry | retry_with_comment: - new_item = create_retry_item(item, lateral_plan) - // worktree 보존 - - hitl: - lateral_report = build_lateral_report(item.source_id, item.state) - create_hitl_event(item, reason, hitl_notes=lateral_report) - // worktree 보존 -``` - -### lateral_plan이 handler에 주입되는 방식 - -retry로 생성된 새 아이템이 다시 Running에 진입하면, `lateral_plan`이 handler prompt에 추가 컨텍스트로 주입된다: - -``` -원래 prompt: "이슈를 구현해줘" - -주입 후: - "이슈를 구현해줘 - - ⚠ Stagnation Analysis (attempt 2/3) - Pattern: SPINNING | Persona: HACKER - - 실패 원인: 이전 2회 시도에서 동일한 컴파일 에러 반복 - 대안 접근법: tower-sessions crate 활용 - 실행 계획: 1. Cargo.toml 수정 2. 타입 교체 3. middleware 등록 - 주의: 이전과 동일한 접근은 같은 실패를 반복합니다" -``` - ---- - -## Dependency Gate (#721) - -### Spec Dependency Gate - -`check_dependency_gate()` — Pending→Ready 전이 시 확인. 스펙 간 의존 관계 확인. - -### Queue Dependency Gate - -`check_queue_dependency_gate()` — Ready→Running 전이 시 확인. - -dependency phase 확인은 **DB 조회 기반**: - -``` -1. DB에서 dependency work_id 목록 조회 -2. 각 dependency의 phase를 DB에서 조회 -3. 판정: - - Done → gate open - - DB에 없음 → gate open (orphan 허용) - - 그 외 → gate blocked -``` - -### Conflict Gate - -`check_conflict_gate()` — entry_point 겹침 감지. DB 기반. - ---- - -## Handler와 Hook의 분리 - -### Handler — yaml에 정의된 작업 - -```yaml -handlers: - - prompt: "..." # → AgentRuntime.invoke() (LLM, worktree 안에서) - - script: "..." # → bash 실행 (결정적, WORK_ID + WORKTREE 주입) -``` - -handler는 Daemon Executor가 직접 실행한다. 작업의 핵심 로직. - -### Hook — LifecycleHook trait impl - -| hook | 트리거 시점 | 실행 책임 | -|------|-----------|----------| -| `on_enter` | Running 진입 후, handler 실행 전 | Hook impl | -| `on_done` | evaluate Done 판정 후 | Hook impl | -| `on_fail` | 실패 시 (retry 제외) | Hook impl | -| `on_escalation` | escalation 결정 후 | Hook impl | - -Daemon은 hook을 트리거만 한다. hook이 실제로 무엇을 하는지 모른다. -상세: [LifecycleHook](./lifecycle-hook.md) - ---- - -## 환경변수 - -| 변수 | 설명 | -|------|------| -| `WORK_ID` | 큐 아이템 식별자 | -| `WORKTREE` | worktree 경로 | - -나머지는 `belt context $WORK_ID --json`으로 조회. - ---- - -## Graceful Shutdown - -``` -SIGINT → on_shutdown: - 1. Running 아이템 완료 대기 (timeout: 30초) - → timeout 초과: Pending으로 롤백, worktree 보존 - 2. Cron engine 정지 -``` - ---- - -## 수용 기준 - -### Daemon = CPU - -- [ ] Daemon은 상태 머신 순회 + hook 트리거만 담당한다 -- [ ] 상태 전이 시 workspace의 LifecycleHook.on_*()을 트리거한다 -- [ ] hook의 실행 결과(Result)만 받고, 구체적 동작을 모른다 - -### 내부 모듈 구조 (#717) - -- [ ] phase 전이는 Advancer, handler 실행+hook 트리거+stagnation+lateral은 Executor, HITL은 HitlService -- [ ] 각 모듈은 독립적으로 단위 테스트 가능하다 -- [ ] 모듈 간 의존은 trait 또는 함수 파라미터로만 전달 (순환 참조 금지) - -### Stagnation + Lateral 통합 (#723) - -- [ ] handler/on_enter 실패 시 StagnationDetector가 항상 실행된다 -- [ ] CompositeSimilarity로 outputs/errors를 별도 검사한다 -- [ ] 패턴 감지 시 LateralAnalyzer가 내장 페르소나로 lateral_plan을 생성한다 -- [ ] lateral_plan이 retry 시 handler prompt에 추가 컨텍스트로 주입된다 -- [ ] hitl 도달 시 모든 lateral 시도 이력이 hitl_notes에 첨부된다 -- [ ] stagnation 이벤트가 transition_events에 기록된다 - -### Dependency Gate (#721) - -- [ ] queue dependency의 phase 확인은 DB 조회 기준이다 -- [ ] 재시작 후에도 dependency gate가 정확히 동작한다 -- [ ] dependency가 Failed/Hitl이면 blocked, DB에 없으면 open - -### Concurrency - -- [ ] workspace.concurrency + daemon.max_concurrent 2단계 제한 -- [ ] evaluate LLM 호출도 concurrency slot 소비 - -### Graceful Shutdown - -- [ ] SIGINT → 30초 대기 → Pending 롤백 + worktree 보존 - -### 환경변수 - -- [ ] handler/script에 WORK_ID, WORKTREE 2개만 주입 - ---- - -### 관련 문서 - -- [DESIGN-v6](../DESIGN-v6.md) — 전체 상태 흐름 + 설계 철학 -- [LifecycleHook](./lifecycle-hook.md) — 상태 전이 반응 trait -- [QueuePhase 상태 머신](./queue-state-machine.md) — 상태 전이 상세 -- [Stagnation Detection](./stagnation.md) — Composite Similarity + Lateral Thinking -- [DataSource](./datasource.md) — 수집/컨텍스트 추상화 -- [AgentRuntime](./agent-runtime.md) — LLM 실행 추상화 -- [Cron 엔진](./cron-engine.md) — evaluate cron + 품질 루프 diff --git a/spec/draft/flows/03-issue-pipeline.md b/spec/draft/flows/03-issue-pipeline.md deleted file mode 100644 index 9d6844a..0000000 --- a/spec/draft/flows/03-issue-pipeline.md +++ /dev/null @@ -1,147 +0,0 @@ -# Flow 3: 이슈 파이프라인 — 컨베이어 벨트 - -> 이슈가 DataSource의 상태 정의에 따라 자동으로 처리되고, Done이 다음 단계를 트리거한다. - ---- - -## 컨베이어 벨트 흐름 - -``` -belt:analyze 감지 → [analyze handlers] → evaluate → on_done script → belt:implement 부착 - │ -belt:implement 감지 → [implement handlers] → evaluate → on_done script → belt:review 부착 - │ -belt:review 감지 → [review handlers] → evaluate → on_done script → belt:done 부착 -``` - -각 구간은 독립적인 QueueItem. 되돌아가지 않고, 항상 새 아이템으로 다음 구간에 진입. - ---- - -## 단일 구간 상세 - -``` -DataSource.collect(): trigger 조건 매칭 (예: belt:analyze 라벨) - │ - ▼ - Pending → Ready → Running (자동 전이, concurrency 제한) - │ - │ ① worktree 생성 (인프라, 또는 retry 시 기존 보존분 재사용) - │ ② hook.on_enter() 트리거 (workspace의 LifecycleHook) - │ ③ handlers 순차 실행: - │ prompt → AgentRuntime.invoke() (worktree 안에서) - │ script → bash (WORK_ID + WORKTREE 주입) - │ - ├── 전부 성공 → Completed - │ │ - │ ▼ - │ evaluate (per-item, concurrency slot 소비): - │ "이 아이템의 결과가 충분한가?" - │ ├── Done → hook.on_done() 트리거 → worktree 정리 - │ │ └── hook 실패 → Failed (로그 기록) - │ └── HITL → HITL 이벤트 생성 → 사람 대기 (worktree 보존) - │ - └── 실패 (handler 또는 on_enter) - │ - ▼ - Stagnation 분석 (항상 실행): - 이전 시도와 유사한 패턴인가? (반복, 교대 등) - 패턴 감지 시 → 다른 접근법으로 전환하여 재시도 - │ - ▼ - Escalation (실패 횟수 기반): - ├── 1회 → 조용히 재시도 (대안 접근법 주입) - ├── 2회 → 외부 시스템에 알림 + 재시도 - └── 3회 → 사람에게 전달 (lateral report 첨부) - └── 사람: done/retry/skip/replan - └── timeout → terminal (skip 또는 replan) - 상세: [실패 복구와 HITL](./04-failure-and-hitl.md) -``` - ---- - -## on_done script 예시 - -on_done script는 `belt context`로 필요한 정보를 조회하여 외부 시스템에 결과를 반영한다. - -```yaml -on_done: - - script: | - CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') - REPO=$(echo $CTX | jq -r '.source.url') - TITLE=$(echo $CTX | jq -r '.source_data.issue.title // .issue.title') - gh pr create --title "$TITLE" --body "Closes #$ISSUE" -R $REPO - gh issue edit $ISSUE --remove-label "belt:implement" -R $REPO - gh issue edit $ISSUE --add-label "belt:review" -R $REPO -``` - -Daemon이 주입하는 환경변수는 `WORK_ID`와 `WORKTREE`뿐. 이슈 번호, 레포 URL 등은 `belt context`로 직접 조회한다. - ---- - -## 피드백 루프 - -### PR review comment (changes-requested) - -``` -DataSource.collect()가 changes-requested 감지 - → 새 아이템 생성 → handlers 실행 → 수정 반영 -``` - -### /spec update - -``` -스펙 변경 → on_spec_active → Cron(gap-detection) 재평가 - → gap 발견 시 새 이슈 생성 → 파이프라인 재진입 -``` - -### 핵심 원칙 - -**스펙 = 계약**. 계약이 바뀌어야 하면 `/spec update`. 계약 범위 내 작업이면 이슈 등록. - ---- - -## 인프라 오류와 Circuit Breaker - -handler 로직 실패(stagnation)와 인프라 오류는 다른 문제이다. - -| 구분 | 예시 | 대응 | -|------|------|------| -| **handler 실패** | 컴파일 에러, 테스트 실패 | stagnation 분석 → lateral thinking → escalation | -| **인프라 오류** | GitHub API 장애, 디스크 부족, worktree 깨짐 | circuit breaker → 일시 중단 → 자동 복구 | - -### 인프라 오류 발생 시 - -``` -인프라 오류 감지 (GitHub API 5xx, worktree 생성 실패 등) - │ - ├── dashboard에 오류 상태 표시 - │ "⚠ github:org/repo — GitHub API 503 (3/5 failures)" - │ - ├── backoff retry (1s → 2s → 4s → ...) - │ - └── Circuit Breaker: - closed (정상) - → N회 연속 인프라 오류 → open - open (중단) - → 해당 source/작업 일시 중단 - → dashboard에 "🔴 circuit open" 표시 - → cooldown 후 half-open - half-open (시험) - → 1건 시험 실행 - → 성공 → closed (정상 복귀) - → 실패 → open (다시 중단) -``` - -circuit breaker는 source 단위로 동작한다. 한 source의 인프라 오류가 다른 workspace의 작업에 영향을 주지 않는다. - ---- - -### 관련 문서 - -- [DataSource](../concerns/datasource.md) — 상태 기반 워크플로우 + context 스키마 -- [LifecycleHook](../concerns/lifecycle-hook.md) — 상태 전이 반응 -- [실패 복구와 HITL](./04-failure-and-hitl.md) — escalation 정책 -- [Stagnation Detection](../concerns/stagnation.md) — 실패 패턴 감지 -- [Evaluator](../concerns/evaluator.md) — Progressive Evaluation Pipeline diff --git a/spec/draft/flows/04-failure-and-hitl.md b/spec/draft/flows/04-failure-and-hitl.md deleted file mode 100644 index 7f9e098..0000000 --- a/spec/draft/flows/04-failure-and-hitl.md +++ /dev/null @@ -1,236 +0,0 @@ -# Flow 4: 실패 복구와 HITL - -> handler 실패 시 stagnation 분석 + lateral thinking으로 사고를 전환하여 재시도하고, evaluate가 HITL로 분류하면 사람의 판단을 요청한다. - ---- - -## 실패 경로 - -``` -handler 또는 hook.on_enter() 실패 - │ - ▼ -Stagnation 분석 (항상 실행): - 이전 시도 기록을 분석하여 정체 패턴을 감지: - │ - ├── 같은 실패 반복 (SPINNING) - ├── A↔B 교대 반복 (OSCILLATION) - └── (Phase 2) 진행 정체, 개선폭 감소 - │ - ├── 패턴 없음 ─────── escalation 적용 (기존 방식으로 재시도) - │ - └── 패턴 감지 ─┐ - ▼ - 사고 전환 (Lateral Thinking): - 패턴에 맞는 다른 접근법을 선택 (이전 시도와 중복 없이) - 예: 반복 실패 → 워크어라운드 시도, 교대 반복 → 구조 재설계 - → 대안 접근 계획(lateral plan) 생성 - │ - ▼ - Escalation (실패 횟수 기반, lateral plan 포함): - │ - ├── retry → hook.on_escalation(retry), lateral_plan 주입, 재시도 (worktree 보존) - ├── retry_with_comment → hook.on_escalation + hook.on_fail, lateral_plan 주입, 재시도 - └── hitl → hook.on_escalation + hook.on_fail, lateral_report 첨부, HITL 이벤트 - └── 사람 응답: done / retry / skip / replan - └── timeout → terminal 액션 (skip 또는 replan) -``` - -`retry`만 on_fail hook을 트리거하지 않는다. "조용한 재시도"로 외부 시스템에 노이즈를 주지 않는다. -Daemon은 hook을 트리거만 하고, 실행 책임은 workspace의 LifecycleHook impl이 가진다. - ---- - -## Lateral Plan 주입 예시 - -retry로 생성된 새 아이템이 다시 Running에 진입하면, lateral_plan이 handler prompt에 추가 컨텍스트로 주입된다: - -``` -원래 handler prompt: - "이슈를 구현해줘" - -lateral retry 시 합성: - "이슈를 구현해줘 - - ⚠ Stagnation Analysis (attempt 2/3) - Pattern: SPINNING | Persona: HACKER - - 실패 원인: 이전 2회 시도에서 동일한 컴파일 에러 반복 - error[E0433]: cannot find type Session in auth::middleware - 대안 접근법: 기존 Session 직접 구현 대신 tower-sessions crate 활용 - 실행 계획: - 1. Cargo.toml에 tower-sessions 추가 - 2. Session 타입 참조를 교체 - 3. middleware에 SessionManagerLayer 등록 - 주의: 이전과 동일한 접근은 같은 실패를 반복합니다" -``` - ---- - -## Escalation 정책 (workspace yaml 소유) - -```yaml -sources: - github: - escalation: - 1: retry - 2: retry_with_comment - 3: hitl - terminal: skip # hitl timeout 시 (skip 또는 replan) - -stagnation: - enabled: true - similarity: - - judge: exact_hash - weight: 0.5 - - judge: token_fingerprint - weight: 0.3 - - judge: ncd - weight: 0.2 - lateral: - enabled: true - max_attempts: 3 -``` - -- escalation 레벨은 기존과 동일 (failure_count 기반) -- stagnation + lateral은 **모든 retry의 품질을 투명하게 높이는 내장 레이어** -- `stagnation.enabled: false`이면 lateral 없이 기존 v5 동작 - -### on_fail script 예시 - -```yaml -on_fail: - - script: | - CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') - REPO=$(echo $CTX | jq -r '.source.url') - FAILURES=$(echo $CTX | jq '[.history[] | select(.status=="failed")] | length') - gh issue comment $ISSUE --body "실패 (시도 횟수: $FAILURES)" -R $REPO -``` - ---- - -## HITL (Human-in-the-Loop) - -### 생성 경로 - -| 경로 | 트리거 | -|------|--------| -| Escalation | handler/on_enter 실패 → failure_count=3 → hitl | -| evaluate | handler 성공 → evaluate가 "사람이 봐야 한다" 판단 | -| 스펙 완료 | 모든 linked issues Done → 최종 확인 요청 | -| 충돌 | DependencyGuard가 스펙 충돌 감지 | - -### HITL 진입 — LLM이 질문을 구성 - -HITL에 진입하면 LLM이 상황(lateral report, 이력, HITL 경로)을 분석하여 **맥락에 맞는 질문과 추천 선택지**를 구성한다. 고정된 4개 선택지가 아니라, 상황별로 다른 제안이 나온다. - -#### 예시: Escalation HITL (handler 3회 실패) - -``` -"JWT middleware 구현이 3회 실패했습니다. - - 시도 이력: - 1회: compile error (Session not found) - 2회: tower-sessions 시도 → 다른 에러 발생 - 3회: trait object 시도 → 컴파일 성공, 테스트 실패 - - 추천: - 1. axum-sessions crate로 전환하여 재시도 - 2. Session 관련 코드를 별도 이슈로 분리 - 3. 현재 결과로 PR 생성 (부분 완료) - 4. 이 이슈 건너뛰기 - 또는 직접 지시를 입력하세요" -``` - -#### 예시: Evaluate HITL (완료 여부 불확실) - -``` -"JWT middleware 구현 결과를 검토했으나 확신이 부족합니다. - - 현재 상태: - - 컴파일 성공, 테스트 18/20 통과 - - 실패 테스트: session expiry, concurrent access - - 추천: - 1. 실패 테스트 2건을 수정하여 재시도 - 2. 현재 상태로 PR 생성 (실패 테스트는 후속 이슈로) - 3. 전체 접근 방식을 재검토 - 또는 직접 지시를 입력하세요" -``` - -#### 예시: Spec 완료 HITL - -``` -"스펙 'JWT 인증 시스템'의 모든 이슈가 완료되었습니다. - - 완료된 이슈: #42 middleware, #43 token 발급, #44 refresh - gap-detection: 추가 gap 미발견 - 테스트 커버리지: 87% - - 추천: - 1. 스펙 완료 승인 - 2. 추가 검증 항목 지정하여 재검토 - 또는 직접 지시를 입력하세요" -``` - -### 응답 처리 - -사용자는 번호 선택 또는 자연어로 응답한다. LLM이 응답을 해석하여 시스템 액션으로 변환한다. - -``` -사용자 응답 (TUI / CLI / /agent 세션) - │ - ▼ -LLM이 응답 해석 → 시스템 액션으로 변환: - │ - ├── Done(plan) → hook.on_done() 트리거 → worktree 정리 - ├── Retry(plan) → 사용자 지시를 lateral_plan으로 주입 - │ 새 아이템 생성 → Pending (worktree 보존) - ├── Skip → Skipped (worktree 정리) - └── Replan → 스펙 수정 제안 (아래 참조) -``` - -### Replan - -``` -replan (사용자 응답 또는 hitl timeout) - → replan_count 증가 (max 3) - → max 초과 시: Skipped (worktree 정리) - → max 이내: - 1. Agent가 실패 컨텍스트 + lateral report를 분석하여 스펙 수정 제안 - 2. 사용자가 /spec update로 스펙 수정 → 새 이슈 생성 → 파이프라인 재진입 -``` - -### 타임아웃 - -``` -기본: 24시간 -초과 시: hitl-timeout cron (5분 주기)이 감지 - → escalation.terminal 설정에 따라: - skip → Skipped (worktree 정리) - replan → replan 처리 (위 참조) -``` - ---- - -## Graceful Shutdown - -``` -SIGINT → on_shutdown: - 1. Running 아이템 완료 대기 (timeout: 30초) - → timeout 초과: Pending으로 롤백, worktree 보존 - 2. Cron engine 정지 -``` - ---- - -### 관련 문서 - -- [Stagnation Detection](../concerns/stagnation.md) — 패턴 탐지 + Lateral Thinking -- [Daemon](../concerns/daemon.md) — handle_failure + hook 트리거 -- [LifecycleHook](../concerns/lifecycle-hook.md) — 상태 전이 반응 -- [Evaluator](../concerns/evaluator.md) — Progressive Evaluation Pipeline -- [Agent](../concerns/agent-workspace.md) — 대화형 에이전트 (HITL 질문 구성) -- [이슈 파이프라인](./03-issue-pipeline.md) — 실패가 발생하는 실행 흐름 -- [Data Model](../concerns/data-model.md) — HitlReason, EscalationAction enum diff --git a/spec/flows/01-setup.md b/spec/flows/01-setup.md index 2257e2f..ec7e651 100644 --- a/spec/flows/01-setup.md +++ b/spec/flows/01-setup.md @@ -66,6 +66,9 @@ sources: 3: hitl terminal: skip # hitl timeout 시 적용 (skip 또는 replan) + # 참고: on_done/on_fail은 v6 Phase 1에서 ScriptLifecycleHook 어댑터로 처리됨. + # Phase 2에서 DataSource별 LifecycleHook impl로 대체 예정. + runtime: default: claude claude: @@ -87,6 +90,30 @@ workspace는 하나의 외부 레포와 1:1로 대응한다. GitHub 기준으로 6. Agent 워크스페이스 초기화 확인 ``` +### 에러 시나리오 + +CLI가 등록 전에 즉시 검증하고, 실패 시 구체적 에러 메시지를 표시한다. + +``` +belt workspace add --config workspace.yaml + + yaml 파싱 실패: + → "Error: workspace.yaml:12 — 'handlers' 필드가 필요합니다" + + repo 접근 불가: + → "Error: https://github.com/org/repo 접근 불가 (401 Unauthorized)" + → "hint: gh auth status로 인증 상태를 확인하세요" + + workspace 이름 중복: + → "Error: workspace 'auth-project'가 이미 존재합니다" + → "hint: belt workspace remove auth-project로 기존 workspace를 삭제하세요" + + DataSource 유형 미지원: + → "Error: 'jira' DataSource는 아직 지원되지 않습니다 (v7+)" +``` + +모든 검증은 DB 기록 전에 수행된다. 실패 시 부수효과 없음. + --- ## 2. 컨벤션 부트스트랩 diff --git a/spec/flows/02-spec-lifecycle.md b/spec/flows/02-spec-lifecycle.md index 60025f2..347f99d 100644 --- a/spec/flows/02-spec-lifecycle.md +++ b/spec/flows/02-spec-lifecycle.md @@ -7,21 +7,22 @@ ## Spec Lifecycle ``` -Draft ──→ Active ←──→ Paused - │ - ▼ - Completing - │ - ▼ - Completed (terminal) +Draft ──→ Analyzing ──→ Active ←──→ Paused + │ │ + │ 문제 발견 ▼ + └→ Draft Completing + │ + ▼ + Completed (terminal) Any ──→ Archived (soft delete) Archived ──resume──→ Active (복구) ``` -| 상태 | 가능한 전이 | CLI | -|------|------------|-----| -| Draft | → Active | `spec add` | +| 상태 | 가능한 전이 | CLI / 트리거 | +|------|------------|-------------| +| Draft | → Analyzing | `spec add` | +| Analyzing | → Active(분석 통과), → Draft(문제 발견) | 자동 (LLM 분석) | | Active | → Paused, → Completing(자동), → Archived | `spec pause`, `spec remove` | | Paused | → Active, → Archived | `spec resume`, `spec remove` | | Completing | → Active(gap 발견), → Completed(HITL 승인) | 자동 | @@ -30,17 +31,50 @@ Archived ──resume──→ Active (복구) --- -## 등록 → 이슈 분해 +## 등록 → 분석 → 이슈 분해 ``` /spec add [file] - → 필수 섹션 검증 (개요, 요구사항, 아키텍처, 테스트, 수용 기준) - → DB에 저장 (status: Active) + → 필수 섹션 검증 (기계적: 개요, 요구사항, 아키텍처, 테스트, 수용 기준) + → DB에 저장 (status: Analyzing) + │ + ▼ +Analyzing: + LLM이 스펙을 분석 (설정: ~/.belt/config.yaml) + ├── quality — 섹션 완성도, AC 구체성 + ├── decomposability — 이슈로 분해 가능한지 + └── dependency — 기존 스펙과 충돌/의존 + │ + ├── 분석 점수 >= auto_approve_threshold → Active로 자동 전이 + ├── 점수 미달 → 사용자에게 피드백 + Draft로 복귀 + │ "AC #3이 검증 불가능합니다. 구체적 기대 결과를 추가하세요" + │ "스펙 'auth-v2'와 의존 관계가 감지되었습니다" + └── 분석 실패 (LLM 오류) → Draft로 복귀 + 에러 표시 + │ + ▼ +Active: → 스펙 분해 → 이슈 자동 생성 → 각 이슈에 trigger 라벨 (예: belt:analyze) 부착 → DataSource.collect()가 감지 → 파이프라인 진입 ``` +### 분석 설정 + +`~/.belt/config.yaml`에서 글로벌 기본값을 정의하고, workspace yaml에서 오버라이드 가능하다. + +```yaml +# ~/.belt/config.yaml +spec: + analysis: + runtime: claude # 분석에 사용할 LLM + model: sonnet + checks: # 활성화할 분석 항목 + - quality # 섹션 완성도, AC 구체성 + - decomposability # 이슈로 분해 가능한지 + - dependency # 다른 스펙과 충돌/의존 + auto_approve_threshold: 0.8 # 이 점수 이상이면 자동으로 Active 전이 +``` + ### 스펙 분해 전략 스펙 분해는 **built-in skill**로 제공되며, 커스텀 오버라이드가 가능하다. diff --git a/spec/flows/03-issue-pipeline.md b/spec/flows/03-issue-pipeline.md index b15975a..9d6844a 100644 --- a/spec/flows/03-issue-pipeline.md +++ b/spec/flows/03-issue-pipeline.md @@ -27,7 +27,7 @@ DataSource.collect(): trigger 조건 매칭 (예: belt:analyze 라벨) Pending → Ready → Running (자동 전이, concurrency 제한) │ │ ① worktree 생성 (인프라, 또는 retry 시 기존 보존분 재사용) - │ ② on_enter script 실행 (정의된 경우) + │ ② hook.on_enter() 트리거 (workspace의 LifecycleHook) │ ③ handlers 순차 실행: │ prompt → AgentRuntime.invoke() (worktree 안에서) │ script → bash (WORK_ID + WORKTREE 주입) @@ -35,18 +35,27 @@ DataSource.collect(): trigger 조건 매칭 (예: belt:analyze 라벨) ├── 전부 성공 → Completed │ │ │ ▼ - │ evaluate cron (force_trigger로 즉시 실행 + 주기 폴링 하이브리드): - │ "완료? 추가 검토?" - │ ├── Done → on_done script 실행 → worktree 정리 - │ │ └── script 실패 → Failed (로그 기록, 재시도 가능) + │ evaluate (per-item, concurrency slot 소비): + │ "이 아이템의 결과가 충분한가?" + │ ├── Done → hook.on_done() 트리거 → worktree 정리 + │ │ └── hook 실패 → Failed (로그 기록) │ └── HITL → HITL 이벤트 생성 → 사람 대기 (worktree 보존) │ - └── 실패 (handler 또는 on_enter) → escalation 정책 적용 - ├── retry → 새 아이템 생성, worktree 보존 - ├── retry_with_comment → on_fail + 새 아이템 생성 - └── hitl → on_fail + HITL 생성 - └── 사람: done/retry/skip/replan - └── timeout → terminal (skip 또는 replan) + └── 실패 (handler 또는 on_enter) + │ + ▼ + Stagnation 분석 (항상 실행): + 이전 시도와 유사한 패턴인가? (반복, 교대 등) + 패턴 감지 시 → 다른 접근법으로 전환하여 재시도 + │ + ▼ + Escalation (실패 횟수 기반): + ├── 1회 → 조용히 재시도 (대안 접근법 주입) + ├── 2회 → 외부 시스템에 알림 + 재시도 + └── 3회 → 사람에게 전달 (lateral report 첨부) + └── 사람: done/retry/skip/replan + └── timeout → terminal (skip 또는 replan) + 상세: [실패 복구와 HITL](./04-failure-and-hitl.md) ``` --- @@ -59,9 +68,9 @@ on_done script는 `belt context`로 필요한 정보를 조회하여 외부 시 on_done: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.issue.number') + ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') REPO=$(echo $CTX | jq -r '.source.url') - TITLE=$(echo $CTX | jq -r '.issue.title') + TITLE=$(echo $CTX | jq -r '.source_data.issue.title // .issue.title') gh pr create --title "$TITLE" --body "Closes #$ISSUE" -R $REPO gh issue edit $ISSUE --remove-label "belt:implement" -R $REPO gh issue edit $ISSUE --add-label "belt:review" -R $REPO @@ -93,8 +102,46 @@ DataSource.collect()가 changes-requested 감지 --- +## 인프라 오류와 Circuit Breaker + +handler 로직 실패(stagnation)와 인프라 오류는 다른 문제이다. + +| 구분 | 예시 | 대응 | +|------|------|------| +| **handler 실패** | 컴파일 에러, 테스트 실패 | stagnation 분석 → lateral thinking → escalation | +| **인프라 오류** | GitHub API 장애, 디스크 부족, worktree 깨짐 | circuit breaker → 일시 중단 → 자동 복구 | + +### 인프라 오류 발생 시 + +``` +인프라 오류 감지 (GitHub API 5xx, worktree 생성 실패 등) + │ + ├── dashboard에 오류 상태 표시 + │ "⚠ github:org/repo — GitHub API 503 (3/5 failures)" + │ + ├── backoff retry (1s → 2s → 4s → ...) + │ + └── Circuit Breaker: + closed (정상) + → N회 연속 인프라 오류 → open + open (중단) + → 해당 source/작업 일시 중단 + → dashboard에 "🔴 circuit open" 표시 + → cooldown 후 half-open + half-open (시험) + → 1건 시험 실행 + → 성공 → closed (정상 복귀) + → 실패 → open (다시 중단) +``` + +circuit breaker는 source 단위로 동작한다. 한 source의 인프라 오류가 다른 workspace의 작업에 영향을 주지 않는다. + +--- + ### 관련 문서 - [DataSource](../concerns/datasource.md) — 상태 기반 워크플로우 + context 스키마 +- [LifecycleHook](../concerns/lifecycle-hook.md) — 상태 전이 반응 - [실패 복구와 HITL](./04-failure-and-hitl.md) — escalation 정책 -- [Cron 엔진](../concerns/cron-engine.md) — evaluate cron + 품질 루프 +- [Stagnation Detection](../concerns/stagnation.md) — 실패 패턴 감지 +- [Evaluator](../concerns/evaluator.md) — Progressive Evaluation Pipeline diff --git a/spec/flows/04-failure-and-hitl.md b/spec/flows/04-failure-and-hitl.md index 9869b75..7f9e098 100644 --- a/spec/flows/04-failure-and-hitl.md +++ b/spec/flows/04-failure-and-hitl.md @@ -1,27 +1,69 @@ # Flow 4: 실패 복구와 HITL -> handler 실행 실패 시 escalation 정책에 따라 복구하고, evaluate가 HITL로 분류하면 사람의 판단을 요청한다. +> handler 실패 시 stagnation 분석 + lateral thinking으로 사고를 전환하여 재시도하고, evaluate가 HITL로 분류하면 사람의 판단을 요청한다. --- ## 실패 경로 ``` -handler 또는 on_enter 실행 실패 +handler 또는 hook.on_enter() 실패 │ ▼ -escalation 정책 적용 (failure_count 기반, history에서 계산): +Stagnation 분석 (항상 실행): + 이전 시도 기록을 분석하여 정체 패턴을 감지: │ - ├── retry → 조용히 재시도 (on_fail 실행 안 함, worktree 보존) - ├── retry_with_comment → on_fail script 실행 + 재시도 (worktree 보존) - └── hitl → on_fail script 실행 + HITL 이벤트 생성 (worktree 보존) + ├── 같은 실패 반복 (SPINNING) + ├── A↔B 교대 반복 (OSCILLATION) + └── (Phase 2) 진행 정체, 개선폭 감소 + │ + ├── 패턴 없음 ─────── escalation 적용 (기존 방식으로 재시도) + │ + └── 패턴 감지 ─┐ + ▼ + 사고 전환 (Lateral Thinking): + 패턴에 맞는 다른 접근법을 선택 (이전 시도와 중복 없이) + 예: 반복 실패 → 워크어라운드 시도, 교대 반복 → 구조 재설계 + → 대안 접근 계획(lateral plan) 생성 + │ + ▼ + Escalation (실패 횟수 기반, lateral plan 포함): + │ + ├── retry → hook.on_escalation(retry), lateral_plan 주입, 재시도 (worktree 보존) + ├── retry_with_comment → hook.on_escalation + hook.on_fail, lateral_plan 주입, 재시도 + └── hitl → hook.on_escalation + hook.on_fail, lateral_report 첨부, HITL 이벤트 └── 사람 응답: done / retry / skip / replan - └── timeout → terminal 액션 적용 (설정에 따라 skip 또는 replan) + └── timeout → terminal 액션 (skip 또는 replan) ``` -`retry`만 on_fail을 실행하지 않는다. "조용한 재시도"로 외부 시스템에 노이즈를 주지 않는다. +`retry`만 on_fail hook을 트리거하지 않는다. "조용한 재시도"로 외부 시스템에 노이즈를 주지 않는다. +Daemon은 hook을 트리거만 하고, 실행 책임은 workspace의 LifecycleHook impl이 가진다. + +--- + +## Lateral Plan 주입 예시 -> **skip/replan**: 독립적인 escalation level이 아니라 hitl의 응답 경로 또는 hitl timeout 시의 `terminal` 설정이다. skip은 terminal 상태(Skipped)이므로, 이후 실패가 발생할 수 없어 순차적 escalation level로 정의할 수 없다. +retry로 생성된 새 아이템이 다시 Running에 진입하면, lateral_plan이 handler prompt에 추가 컨텍스트로 주입된다: + +``` +원래 handler prompt: + "이슈를 구현해줘" + +lateral retry 시 합성: + "이슈를 구현해줘 + + ⚠ Stagnation Analysis (attempt 2/3) + Pattern: SPINNING | Persona: HACKER + + 실패 원인: 이전 2회 시도에서 동일한 컴파일 에러 반복 + error[E0433]: cannot find type Session in auth::middleware + 대안 접근법: 기존 Session 직접 구현 대신 tower-sessions crate 활용 + 실행 계획: + 1. Cargo.toml에 tower-sessions 추가 + 2. Session 타입 참조를 교체 + 3. middleware에 SessionManagerLayer 등록 + 주의: 이전과 동일한 접근은 같은 실패를 반복합니다" +``` --- @@ -34,10 +76,25 @@ sources: 1: retry 2: retry_with_comment 3: hitl - terminal: skip # hitl timeout 시 적용 (skip 또는 replan) + terminal: skip # hitl timeout 시 (skip 또는 replan) + +stagnation: + enabled: true + similarity: + - judge: exact_hash + weight: 0.5 + - judge: token_fingerprint + weight: 0.3 + - judge: ncd + weight: 0.2 + lateral: + enabled: true + max_attempts: 3 ``` -DataSource마다 다른 정책이 가능한 구조. v5는 GitHub 정책만 구현. +- escalation 레벨은 기존과 동일 (failure_count 기반) +- stagnation + lateral은 **모든 retry의 품질을 투명하게 높이는 내장 레이어** +- `stagnation.enabled: false`이면 lateral 없이 기존 v5 동작 ### on_fail script 예시 @@ -45,14 +102,12 @@ DataSource마다 다른 정책이 가능한 구조. v5는 GitHub 정책만 구 on_fail: - script: | CTX=$(belt context $WORK_ID --json) - ISSUE=$(echo $CTX | jq -r '.issue.number') + ISSUE=$(echo $CTX | jq -r '.source_data.issue.number // .issue.number') REPO=$(echo $CTX | jq -r '.source.url') FAILURES=$(echo $CTX | jq '[.history[] | select(.status=="failed")] | length') gh issue comment $ISSUE --body "실패 (시도 횟수: $FAILURES)" -R $REPO ``` -failure_count는 별도 컬럼이 아니라 history의 append-only 이벤트에서 계산. - --- ## HITL (Human-in-the-Loop) @@ -61,40 +116,90 @@ failure_count는 별도 컬럼이 아니라 history의 append-only 이벤트에 | 경로 | 트리거 | |------|--------| -| Escalation | handler 또는 on_enter 실패 → escalation 정책이 HITL 결정 | +| Escalation | handler/on_enter 실패 → failure_count=3 → hitl | | evaluate | handler 성공 → evaluate가 "사람이 봐야 한다" 판단 | | 스펙 완료 | 모든 linked issues Done → 최종 확인 요청 | | 충돌 | DependencyGuard가 스펙 충돌 감지 | -> **DependencyGuard**: 다중 스펙 등록 시 Agent가 기존 Active 스펙과의 충돌/의존성을 분석하여 등록하는 가드. 같은 파일/모듈에 영향을 주는 스펙이 동시에 진행될 때 HITL 이벤트를 생성한다. 상세는 [스펙 생명주기](./02-spec-lifecycle.md)의 "다중 스펙 우선순위" 참조. +### HITL 진입 — LLM이 질문을 구성 + +HITL에 진입하면 LLM이 상황(lateral report, 이력, HITL 경로)을 분석하여 **맥락에 맞는 질문과 추천 선택지**를 구성한다. 고정된 4개 선택지가 아니라, 상황별로 다른 제안이 나온다. + +#### 예시: Escalation HITL (handler 3회 실패) + +``` +"JWT middleware 구현이 3회 실패했습니다. + + 시도 이력: + 1회: compile error (Session not found) + 2회: tower-sessions 시도 → 다른 에러 발생 + 3회: trait object 시도 → 컴파일 성공, 테스트 실패 + + 추천: + 1. axum-sessions crate로 전환하여 재시도 + 2. Session 관련 코드를 별도 이슈로 분리 + 3. 현재 결과로 PR 생성 (부분 완료) + 4. 이 이슈 건너뛰기 + 또는 직접 지시를 입력하세요" +``` -### 응답 경로 +#### 예시: Evaluate HITL (완료 여부 불확실) + +``` +"JWT middleware 구현 결과를 검토했으나 확신이 부족합니다. + + 현재 상태: + - 컴파일 성공, 테스트 18/20 통과 + - 실패 테스트: session expiry, concurrent access + + 추천: + 1. 실패 테스트 2건을 수정하여 재시도 + 2. 현재 상태로 PR 생성 (실패 테스트는 후속 이슈로) + 3. 전체 접근 방식을 재검토 + 또는 직접 지시를 입력하세요" +``` + +#### 예시: Spec 완료 HITL + +``` +"스펙 'JWT 인증 시스템'의 모든 이슈가 완료되었습니다. + + 완료된 이슈: #42 middleware, #43 token 발급, #44 refresh + gap-detection: 추가 gap 미발견 + 테스트 커버리지: 87% + + 추천: + 1. 스펙 완료 승인 + 2. 추가 검증 항목 지정하여 재검토 + 또는 직접 지시를 입력하세요" +``` + +### 응답 처리 + +사용자는 번호 선택 또는 자연어로 응답한다. LLM이 응답을 해석하여 시스템 액션으로 변환한다. ``` 사용자 응답 (TUI / CLI / /agent 세션) - → belt hitl respond --choice N - → 라우팅: - "done" → on_done script 실행 - ├── script 성공 → Done (worktree 정리) - └── script 실패 → Failed (worktree 보존, 로그 기록) - "retry" → 새 아이템 생성 → Pending (worktree 보존) - "skip" → Skipped (worktree 정리) - "replan" → replan 처리 (아래 참조) + │ + ▼ +LLM이 응답 해석 → 시스템 액션으로 변환: + │ + ├── Done(plan) → hook.on_done() 트리거 → worktree 정리 + ├── Retry(plan) → 사용자 지시를 lateral_plan으로 주입 + │ 새 아이템 생성 → Pending (worktree 보존) + ├── Skip → Skipped (worktree 정리) + └── Replan → 스펙 수정 제안 (아래 참조) ``` ### Replan -replan은 반복 실패한 아이템의 접근 방식을 재설계하기 위한 경로이다. - ``` -replan 요청 (사람 응답 또는 hitl timeout) +replan (사용자 응답 또는 hitl timeout) → replan_count 증가 (max 3) - → max 초과 시: Skipped (worktree 정리, 더 이상 재시도 불가) + → max 초과 시: Skipped (worktree 정리) → max 이내: - 1. HitlReason::SpecModificationProposed 이벤트 생성 - 2. Failed 전이 (worktree 보존) - 3. Agent가 실패 컨텍스트(history, 에러 로그)를 분석하여 스펙 수정 제안 - 4. 사용자가 /spec update로 스펙 수정 → 새 이슈 생성 → 파이프라인 재진입 + 1. Agent가 실패 컨텍스트 + lateral report를 분석하여 스펙 수정 제안 + 2. 사용자가 /spec update로 스펙 수정 → 새 이슈 생성 → 파이프라인 재진입 ``` ### 타임아웃 @@ -115,7 +220,6 @@ replan 요청 (사람 응답 또는 hitl timeout) SIGINT → on_shutdown: 1. Running 아이템 완료 대기 (timeout: 30초) → timeout 초과: Pending으로 롤백, worktree 보존 - (재시작 후 기존 worktree를 재사용하여 이어서 진행) 2. Cron engine 정지 ``` @@ -123,6 +227,10 @@ SIGINT → on_shutdown: ### 관련 문서 -- [DataSource](../concerns/datasource.md) — escalation 정책 + on_fail script -- [Agent](../concerns/agent-workspace.md) — 대화형 에이전트 (HITL 응답 경로 포함) +- [Stagnation Detection](../concerns/stagnation.md) — 패턴 탐지 + Lateral Thinking +- [Daemon](../concerns/daemon.md) — handle_failure + hook 트리거 +- [LifecycleHook](../concerns/lifecycle-hook.md) — 상태 전이 반응 +- [Evaluator](../concerns/evaluator.md) — Progressive Evaluation Pipeline +- [Agent](../concerns/agent-workspace.md) — 대화형 에이전트 (HITL 질문 구성) - [이슈 파이프라인](./03-issue-pipeline.md) — 실패가 발생하는 실행 흐름 +- [Data Model](../concerns/data-model.md) — HitlReason, EscalationAction enum diff --git a/spec/flows/05-monitoring.md b/spec/flows/05-monitoring.md index 2bd6fca..4cad28f 100644 --- a/spec/flows/05-monitoring.md +++ b/spec/flows/05-monitoring.md @@ -46,11 +46,12 @@ │ ⚠ #39 Auth refactor (failed) ││ evaluate 대기 │ │ ⏳ #46 Missing tests ││ │ ├──────────────────────────────────────┤├────────────────────────┤ -│ Orphan: 0 | HITL: 1 pending ││ Logs │ +│ Orphan: 0 | HITL: 1 | Stag: 0 ││ Logs │ │ Kanban: 0P | 0Re | 1Ru | 1C | 2D ││ ... │ └──────────────────────────────────────┘└────────────────────────┘ ``` +> **v6**: Stagnation 카운터 추가 — 현재 stagnation이 감지된 아이템 수를 표시. > **Kanban 약어**: P=Pending, Re=Ready, Ru=Running, C=Completed, D=Done, H=HITL, S=Skipped, F=Failed ### 전이 타임라인 (ItemDetail 오버레이, Enter) @@ -73,24 +74,32 @@ └───────────────────────────────────────────────┘ ``` -### 실패 아이템 타임라인 +### 실패 + Lateral Thinking 타임라인 (v6) ``` ┌─ #39 Auth refactor ──────────────────────────┐ -│ Phase: Failed | Runtime: claude/sonnet │ +│ Phase: HITL | Runtime: claude/sonnet │ +│ Stagnation: SPINNING (score: 0.95) │ │ │ │ Timeline: │ │ 13:00 ○ Pending ← github.collect() │ -│ 13:00 ○ Running │ -│ └ handler: claude/sonnet (2.1K, 8m) │ -│ 13:08 ○ Completed │ -│ 13:08 ○ evaluate → Done │ -│ 13:08 ✗ on_done script (exit 1, 0.5s) │ -│ └ error: gh pr create rate limited │ -│ 13:08 ● Failed │ -│ └ worktree 보존: /tmp/belt/auth-39 │ +│ 13:00 ○ Running (attempt 1) │ +│ └ handler: compile error │ +│ 13:08 ⟳ SPINNING detected (score: 0.95) │ +│ └ lateral: HACKER 페르소나 │ +│ └ plan: tower-sessions crate 시도 │ +│ 13:08 ○ Running (attempt 2, lateral) │ +│ └ handler: 다른 에러 (progress!) │ +│ 13:18 ⟳ retry_with_comment (2/3) │ +│ └ lateral: CONTRARIAN 페르소나 │ +│ └ plan: trait object 접근 │ +│ 13:18 ○ Running (attempt 3, lateral) │ +│ └ handler: 컴파일 성공, 테스트 실패 │ +│ 13:28 ● HITL (3/3) │ +│ └ lateral report 첨부 │ +│ └ 2회 사고 전환 후에도 미해결 │ │ │ -│ Actions: [r] retry-script [s] skip │ +│ Actions: [d] done [r] retry [s] skip │ └───────────────────────────────────────────────┘ ``` @@ -127,12 +136,13 @@ ● belt daemon (uptime 2h 15m) Workspaces: - auth-project ● active queue: 1P 1R 1C 2D 1F specs: 2/3 + auth-project ● active queue: 1P 1R 1C 2D 1F specs: 2/3 stag: 0 backend-tasks ● active queue: 0P 0R 0C 5D specs: 1/1 ✓ Runtime: claude/sonnet (45.2K tokens/1h) HITL: 1 pending ⚠ Failed: 1 ⚠ +Stagnation: 0 Next evaluate: 25s ``` @@ -179,6 +189,8 @@ Dependencies: │ on_enter 3 ok │ │ evaluate 12 ok 1 hitl │ │ ⚠ on_done 1 failed │ +│ stagnation 2 detected │ +│ lateral 2 plans │ └──────────────────────────────────┘ ``` @@ -195,43 +207,22 @@ HITL 이벤트가 생성되면 사용자에게 다음 경로로 알린다: | /agent 세션 | 진입 시 HITL 대기 목록 자동 표시 | | on_fail script | escalation=hitl 시 실행 — GitHub 코멘트 등으로 외부 알림 가능 | -별도 push 알림(Slack, email)은 on_fail/on_done script에서 직접 구현한다 (webhook 호출 등). +별도 push 알림(Slack, email)은 LifecycleHook impl에서 처리한다 (webhook 호출 등). + +> **v6**: Stagnation으로 HITL에 진입한 경우, lateral report(시도한 접근법들, 각 분석 결과)가 표시되어 사용자가 지금까지의 접근 전환 이력을 참고할 수 있다. --- ## 6. 데이터 요구사항 -```sql --- 아이템별 전이 이벤트 (append-only) -transition_events ( - id TEXT PRIMARY KEY, - work_id TEXT NOT NULL, - source_id TEXT NOT NULL, -- 계보 추적 - event_type TEXT NOT NULL, -- phase_enter, handler, evaluate, on_done, on_fail, on_enter - phase TEXT, -- Pending, Ready, Running, Completed, Done, HITL, Failed, Skipped - detail TEXT, -- script exit code, prompt result, error message - created_at TEXT NOT NULL -) - --- token 사용량 (AgentRuntime 실행마다 기록) -token_usage ( - id TEXT PRIMARY KEY, - work_id TEXT NOT NULL, - workspace TEXT NOT NULL, - runtime TEXT NOT NULL, -- claude, gemini, codex - model TEXT, -- sonnet, opus, haiku - input_tokens INTEGER, - output_tokens INTEGER, - duration_ms INTEGER, - created_at TEXT NOT NULL -) -``` +모든 전이 이벤트와 토큰 사용량은 DB에 기록된다. 스키마 상세: [Data Model](../concerns/data-model.md) --- ### 관련 문서 -- [DESIGN-v5](../DESIGN-v5.md) — QueuePhase 상태 머신 +- [DESIGN-v6](../DESIGN-v6.md) — QueuePhase 상태 머신 +- [Stagnation Detection](../concerns/stagnation.md) — 반복 패턴 감지 시각화 - [스펙 생명주기](./02-spec-lifecycle.md) — 스펙 진행률 - [실패 복구와 HITL](./04-failure-and-hitl.md) — HITL 오버레이 - [CLI 레퍼런스](../concerns/cli-reference.md) — 전체 커맨드 트리