From 7cee134a24f864c58c7c39d53d1c5b8710f0dd32 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: Sun, 5 Apr 2026 17:59:18 +0900 Subject: [PATCH 1/2] docs(spec): restructure v6 draft with LifecycleHook, Evaluator, and Daemon=CPU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major design changes to spec/draft based on review: - LifecycleHook trait: separate handler (work) from hook (reaction). Daemon triggers hooks on state transitions, Hook impl owns execution. Per-workspace binding, lazy loading for memory optimization. - Evaluator Pipeline: progressive evaluation (Mechanical → Semantic → Consensus). Evaluate before Execute in daemon tick. History-aware pre-check skips unnecessary handler execution. - Daemon = CPU: rename from Orchestrator. Daemon only triggers hooks, doesn't know what they do. Tick order: evaluate → advance → execute → cron. - PatternDetector trait: each detector queries DB directly instead of ExecutionHistory intermediate struct. v6 scope: Spinning + Oscillation. Phase 2: Drift + Diminishing (trait boundary defined, no impl). - Remove stale concepts: escalation acceleration, classify-policy.yaml, batch_size. Escalation and stagnation are orthogonal concerns. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/draft/DESIGN-v6.md | 229 ++++++------ spec/draft/README.md | 15 +- spec/draft/concerns/agent-workspace.md | 46 +-- spec/draft/concerns/cron-engine.md | 45 +-- spec/draft/concerns/daemon.md | 148 ++++---- spec/draft/concerns/data-model.md | 33 +- spec/draft/concerns/datasource.md | 65 ++-- spec/draft/concerns/evaluator.md | 272 ++++++++++++++ spec/draft/concerns/lifecycle-hook.md | 401 +++++++++++++++++++++ spec/draft/concerns/queue-state-machine.md | 7 +- spec/draft/concerns/stagnation.md | 118 +++--- spec/draft/flows/03-issue-pipeline.md | 8 +- spec/draft/flows/04-failure-and-hitl.md | 9 +- 13 files changed, 1044 insertions(+), 352 deletions(-) create mode 100644 spec/draft/concerns/evaluator.md create mode 100644 spec/draft/concerns/lifecycle-hook.md diff --git a/spec/draft/DESIGN-v6.md b/spec/draft/DESIGN-v6.md index b6f959a..c7aafa4 100644 --- a/spec/draft/DESIGN-v6.md +++ b/spec/draft/DESIGN-v6.md @@ -8,15 +8,16 @@ ## 목표 -Daemon을 **상태 머신 + 정의된 시점에 prompt/script를 호출하는 실행기**로 단순화한다. -DataSource가 자기 시스템의 언어로 워크플로우를 정의하되, 코어는 큐만 돌린다. +Daemon을 **상태 머신 + 상태 전이를 트리거하는 CPU**로 단순화한다. +handler(prompt/script)가 작업을 수행하고, LifecycleHook이 상태 변화에 반응한다. 실패 시 **패턴을 감지하고 사고를 전환**하여 같은 실수를 반복하지 않는다. ``` -Daemon이 아는 것 = 큐 상태 머신 + yaml에 정의된 prompt/script 실행 -DataSource가 정의 = 수집 조건(trigger), 처리(handlers), 결과 반영(on_done/on_fail script) -evaluate가 판단 = handler 결과가 충분한지, 사람이 봐야 하는지 (Done or HITL) -stagnation이 감지 = 실패 패턴을 분석하고, 사고를 전환하여 다르게 재시도 +Daemon이 아는 것 = 큐 상태 머신 + 언제 어떤 hook을 트리거할지 +handler가 실행 = yaml에 정의된 prompt/script (작업 자체) +LifecycleHook이 반응 = 상태 전이 시 외부 시스템에 반영 (DataSource별 impl) +evaluate가 판단 = handler 결과가 충분한지, 사람이 봐야 하는지 (Done or HITL) +stagnation이 감지 = 실패 패턴을 분석하고, 사고를 전환하여 다르게 재시도 ``` --- @@ -41,13 +42,13 @@ Belt는 외부 시스템을 trait으로 추상화한다. 코어는 구체적 시 | 경계 | 추상화 | 사용 지점 | |------|--------|----------| -| **이슈 소스** | `DataSource` trait | collect(), get_context() | +| **이슈 소스** | `DataSource` trait | collect(), get_context() — 읽기 | +| **상태 반응** | `LifecycleHook` trait | on_enter/on_done/on_fail/on_escalation — 쓰기 | | **LLM 실행** | `AgentRuntime` trait | handler prompt, evaluate, lateral plan | -| **외부 반영** | on_done/on_fail script (yaml 정의) | 결과 반영 | | **상태 저장** | belt-infra DB 레이어 | 전체 상태 영속화 | | **코드 격리** | belt-infra worktree 레이어 | worktree 생성/정리 | -> **연동 원칙**: 코어는 외부 시스템의 프로토콜/인증을 모른다. trait impl이 각자의 방식으로 연동한다. 새 외부 시스템 추가 = trait impl 추가, 코어 변경 0. 구체적인 연동 방식은 각 concern 문서([DataSource](./concerns/datasource.md), [AgentRuntime](./concerns/agent-runtime.md))에서 정의한다. +> **연동 원칙**: 코어는 외부 시스템의 프로토콜/인증을 모른다. trait impl이 각자의 방식으로 연동한다. 새 외부 시스템 추가 = DataSource + LifecycleHook impl 추가, 코어 변경 0. 구체적인 연동 방식은 각 concern 문서([DataSource](./concerns/datasource.md), [LifecycleHook](./concerns/lifecycle-hook.md), [AgentRuntime](./concerns/agent-runtime.md))에서 정의한다. --- @@ -61,43 +62,47 @@ Belt는 외부 시스템을 trait으로 추상화한다. 코어는 구체적 시 workspace는 하나의 외부 레포와 1:1로 대응한다. v4의 `repo` 개념을 리네이밍. GitHub 외 Jira, Slack 등도 지원하기 위한 추상화 (v5~v6는 GitHub에 집중). -### 3. DataSource가 워크플로우를 소유 +### 3. DataSource가 수집을, LifecycleHook이 반응을 소유 -각 DataSource는 자기 시스템의 상태 표현으로 워크플로우를 정의한다. 코어는 DataSource 내부를 모른다. 상세: [DataSource](./concerns/datasource.md) +DataSource는 외부 시스템에서 아이템을 읽어오고(수집/컨텍스트), LifecycleHook은 상태 변화에 대해 외부 시스템에 쓴다(반응). 같은 외부 시스템이라도 읽기와 쓰기의 관심사가 분리된다. 상세: [DataSource](./concerns/datasource.md), [LifecycleHook](./concerns/lifecycle-hook.md) -### 4. Daemon = Orchestrator +### 4. Daemon = CPU -수집 → 전이 → 실행 → 분류 → 반영 → 스케줄. GitHub 라벨이 뭔지 모르고 yaml대로 실행할 뿐. 내부는 Advancer·Executor·HitlService·StagnationDetector·Evaluator 모듈로 분리. 상세: [Daemon](./concerns/daemon.md) +상태 머신을 틱마다 순회하며 전이를 결정하고, 해당 workspace의 LifecycleHook을 트리거한다. Daemon은 hook이 실제로 무엇을 하는지 모른다 — `Result<()>`만 받을 뿐. 내부는 Advancer·Executor·HitlService·StagnationDetector·Evaluator 모듈로 분리. 상세: [Daemon](./concerns/daemon.md) -### 5. 코드 작업은 항상 worktree +### 5. handler는 작업, hook은 반응 -handler prompt는 항상 git worktree 안에서 실행. worktree 생성/정리는 인프라 레이어 담당. on_done script가 외부 시스템 반영 (gh CLI 등). +handler(prompt/script)는 yaml에 정의된 작업 자체(분석, 구현, 리뷰). LifecycleHook은 상태 전이 시 외부 시스템 반응(PR 생성, 라벨 변경, 코멘트). Daemon은 handler를 실행하고, 전이가 발생하면 hook을 트리거한다. -### 6. 코어가 출구에서 분류 +### 6. 코드 작업은 항상 worktree -evaluate가 Completed 아이템을 per-work_id 단위로 Done or HITL로 분류. LLM이 `belt queue done/hitl` CLI를 직접 호출하여 상태 전이. +handler prompt는 항상 git worktree 안에서 실행. worktree 생성/정리는 인프라 레이어 담당. -### 7. 아이템 계보 (Lineage) +### 7. Evaluate before Execute — 판정이 실행보다 먼저 + +Daemon tick에서 Evaluator가 Executor보다 먼저 동작한다. 비용이 낮은 검증(Mechanical)부터 단계적으로 수행하여, 이전 기록으로 판정 가능하면 handler 실행을 생략한다. Ouroboros의 progressive evaluation을 차용. 상세: [Evaluator](./concerns/evaluator.md) + +### 8. 아이템 계보 (Lineage) 같은 외부 엔티티에서 파생된 아이템은 `source_id`로 연결. 모든 이벤트는 append-only history로 축적. -### 8. 환경변수 최소화 +### 9. 환경변수 최소화 `WORK_ID` + `WORKTREE` 2개만 주입. 나머지는 `belt context $WORK_ID --json`으로 조회. 상세: [DataSource](./concerns/datasource.md) -### 9. Concurrency 제어 +### 10. Concurrency 제어 workspace.concurrency (workspace yaml 루트) + daemon.max_concurrent 2단계. evaluate LLM 호출도 slot 소비. 상세: [Daemon](./concerns/daemon.md) -### 10. Cron은 품질 루프 +### 11. Cron은 품질 루프 파이프라인은 1회성, 품질은 Cron이 지속 감시. gap-detection이 새 이슈 생성 → 파이프라인 재진입. 상세: [Cron 엔진](./concerns/cron-engine.md) -### 11. Phase 전이 캡슐화 +### 12. Phase 전이 캡슐화 `QueueItem.phase` 필드를 직접 대입하지 못하도록 `pub(crate)` + `transit()` 메서드로 캡슐화한다. 모든 전이는 `can_transition_to()` 검증을 경유한다. 상세: [QueuePhase 상태 머신](./concerns/queue-state-machine.md) -### 12. Stagnation Detection + Lateral Thinking — 실패하면 다르게 시도 +### 13. Stagnation Detection + Lateral Thinking — 실패하면 다르게 시도 실패 횟수만으로는 "같은 실수 반복"과 "다른 시도 실패"를 구분할 수 없다. Composite Pattern 기반 유사도 판단(SimilarityJudge)으로 SPINNING·OSCILLATION 패턴을 감지하고, 내장 페르소나(Lateral Thinking)가 접근법을 전환하여 재시도한다. 모든 retry에 lateral plan이 자동 주입되는 것이 기본 동작. 상세: [Stagnation Detection](./concerns/stagnation.md) @@ -131,7 +136,7 @@ workspace.concurrency (workspace yaml 루트) + daemon.max_concurrent 2단계. e │ Running │ │ │ │ ① worktree 생성 (or retry 시 기존 재사용) │ -│ ② on_enter script │ +│ ② hook.on_enter() 트리거 │ │ ③ handlers 순차 실행 │ │ lateral_plan이 있으면 handler prompt에 추가 컨텍스트 주입 │ └────────┬──────────────────────────────────────────┬───────────────┘ @@ -142,45 +147,34 @@ workspace.concurrency (workspace yaml 루트) + daemon.max_concurrent 2단계. e ┌─────────────────┐ ┌──────────────────────────────────────┐ │ Completed │ │ Stagnation Analyzer (항상 실행) │ │ │ │ │ -│ evaluate 대기 │ │ ① ExecutionHistory 구성 │ -│ (per-item) │ │ outputs = DB history.summary │ -│ │ │ errors = DB history.error │ -│ │ │ │ -│ │ │ ② StagnationDetector.detect │ -│ │ │ CompositeSimilarity: │ -│ │ │ ExactHash (w:0.5) │ -│ │ │ TokenFingerprint (w:0.3) │ -│ │ │ NCD (w:0.2) │ +│ Evaluator가 │ │ ① 각 PatternDetector가 DB 직접 조회 │ +│ 다음 tick에서 │ │ SpinningDetector (summary/error) │ +│ 판정 │ │ OscillationDetector (summary) │ │ │ │ │ -│ │ │ outputs → SPINNING? OSCILLATION? │ -│ │ │ errors → SPINNING? (별도) │ -│ │ │ drifts → NO_DRIFT? DIMINISHING?│ -│ │ │ │ -│ │ │ ③ LateralAnalyzer (패턴 감지 시) │ -│ │ │ 패턴 → 페르소나 선택 (기시도 제외)│ -│ │ │ belt agent -p → lateral_plan │ -│ │ │ │ -│ │ │ ④ Escalation (failure_count 기반) │ +│ Progressive: │ │ ② LateralAnalyzer (패턴 감지 시) │ +│ Mechanical │ │ 페르소나 선택 → lateral_plan │ +│ → Semantic │ │ │ +│ → (Consensus) │ │ ③ Escalation (failure_count 기반) │ +│ │ │ hook.on_escalation() 트리거 │ +│ │ │ hook.on_fail() 트리거 (retry 제외)│ │ │ │ retry → lateral_plan 주입 │ -│ │ │ retry_w_com → lateral + on_fail │ │ │ │ hitl → report 첨부 ─────────┐ │ │ │ terminal → skip/replan ─────────┤ │ │ └──────────────────────────────────────┘ │ │ │ │ -│ evaluate cron: │ retry → 새 아이템 │ -│ belt agent -p │ │ │ -│ per-item 판정 │ ┌─────────────┘ │ -│ │ │ │ +│ │ retry → 새 아이템 │ +│ │ │ │ +│ │ │ hook.on_escalation() 트리거 │ +│ │ ┌──────┘ │ └───┬─────────┬────┘ │ │ │ │ ▼ │ 완료 판정 사람 필요 Pending (lateral_plan 보존) │ │ │ │ ▼ │ │ -┌────────┐ │ │ -│on_done │ │ │ -│ script │ │ │ -└─┬───┬──┘ │ │ - │ │ │ │ + hook │ │ +.on_done() │ │ + │ │ │ + ┌─┴──┐ │ │ 성공 실패 │ │ │ │ │ │ ▼ ▼ ▼ │ @@ -203,80 +197,82 @@ workspace.concurrency (workspace yaml 루트) + daemon.max_concurrent 2단계. e ### 상태별 소유 모듈 -| Phase | 소유 모듈 | 핵심 동작 | -|-------|----------|----------| -| Pending | Advancer | spec dependency gate (DB 조회) | -| Ready | Advancer | queue dep gate (DB) + concurrency check | -| Running | Executor | worktree + on_enter + handlers (lateral_plan 주입) | -| Running → 실패 | StagnationDetector + LateralAnalyzer + Executor | 유사도 분석 → 페르소나 사고 전환 → escalation | -| Completed | Evaluator | per-item LLM 판정 (belt agent -p) | -| Done | Executor | on_done script → worktree 정리 | -| HITL | HitlService | 응답 대기 / timeout / terminal action | -| Failed | — | on_done 실패, 인프라 오류 | -| Skipped | — | terminal | +| Phase | 소유 모듈 | 핵심 동작 | Hook 트리거 | +|-------|----------|----------|------------| +| Pending | Advancer | spec dependency gate (DB 조회) | — | +| Ready | Advancer | queue dep gate (DB) + concurrency check | — | +| Running | Executor | worktree + handlers (lateral_plan 주입) | hook.on_enter() | +| Running → 실패 | StagnationDetector + LateralAnalyzer | 유사도 분석 → 사고 전환 → escalation | hook.on_escalation() + hook.on_fail() | +| Completed | Evaluator | Progressive Pipeline: Mechanical → Semantic → (Consensus) | — | +| Done | — | worktree 정리 | hook.on_done() | +| HITL | HitlService | 응답 대기 / timeout / terminal action | — | +| Failed | — | hook.on_done() 실패, 인프라 오류 | — | +| Skipped | — | terminal | — | --- ## Daemon 내부 구조 ``` -┌─ Daemon (Orchestrator) ───────────────────────────────────────────────┐ +┌─ Daemon (CPU) ────────────────────────────────────────────────────────┐ +│ │ +│ loop { collector → evaluator → advancer → executor → cron.tick() } │ │ │ -│ loop { collector → advancer → executor → cron_engine.tick() } │ +│ Daemon이 아는 것: 상태 머신 + 언제 어떤 hook을 트리거할지 │ +│ Daemon이 모르는 것: hook이 실제로 무엇을 하는지 │ │ │ │ ┌──────────┐ ┌───────────────────────────────────────────────────┐ │ │ │ Advancer │ │ Executor │ │ │ │ │ │ │ │ -│ │ 전이 │ │ handler/lifecycle 실행 │ │ -│ │ dep gate │ │ │ │ │ -│ │ (DB) │ │ ▼ 실패 시 │ │ -│ │ conflict │ │ ┌─────────────────────────────────────────────┐ │ │ -│ │ concurr. │ │ │ StagnationDetector │ │ │ -│ │ │ │ │ │ │ │ +│ │ 전이 │ │ ① hook.on_enter() 트리거 │ │ +│ │ dep gate │ │ ② handler 실행 (yaml prompt/script) │ │ +│ │ (DB) │ │ ③ 성공 → transit(Completed) │ │ +│ │ conflict │ │ → hook.on_done() 트리거 │ │ +│ │ concurr. │ │ ④ 실패 시: │ │ +│ │ │ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ │ │ StagnationDetector │ │ │ │ │ │ │ │ judge: Box │ │ │ -│ │ │ │ │ ┌── CompositeSimilarity ────────────────┐ │ │ │ -│ │ │ │ │ │ ExactHash (w: 0.5) │ │ │ │ -│ │ │ │ │ │ TokenFingerprint (w: 0.3) │ │ │ │ -│ │ │ │ │ │ NCD (w: 0.2) │ │ │ │ -│ │ │ │ │ └───────────────────────────────────────┘ │ │ │ +│ │ │ │ │ └── CompositeSimilarity │ │ │ +│ │ │ │ │ ExactHash / TokenFingerprint / NCD │ │ │ │ │ │ │ └────────────────┬────────────────────────────┘ │ │ -│ │ │ │ │ 패턴 감지 시 │ │ -│ │ │ │ ▼ │ │ +│ │ │ │ ▼ 패턴 감지 시 │ │ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ │ │ LateralAnalyzer │ │ │ -│ │ │ │ │ │ │ │ -│ │ │ │ │ 패턴 → 페르소나 (내장, include_str!) │ │ │ -│ │ │ │ │ SPINNING → HACKER │ │ │ -│ │ │ │ │ OSCILLATION → ARCHITECT │ │ │ -│ │ │ │ │ NO_DRIFT → RESEARCHER │ │ │ -│ │ │ │ │ DIMINISHING → SIMPLIFIER │ │ │ -│ │ │ │ │ 복합 → CONTRARIAN │ │ │ -│ │ │ │ │ │ │ │ -│ │ │ │ │ belt agent -p → lateral_plan 생성 │ │ │ +│ │ │ │ │ 패턴 → 페르소나 → lateral_plan 생성 │ │ │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ -│ │ │ │ │ │ -│ │ │ │ escalation 적용 (lateral_plan 주입) │ │ +│ │ │ │ ▼ │ │ +│ │ │ │ escalation 결정 (failure_count) │ │ +│ │ │ │ → hook.on_escalation(action) 트리거 │ │ +│ │ │ │ → hook.on_fail() 트리거 (retry 제외) │ │ │ └──────────┘ └───────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │ │ │ Evaluator │ │ HitlService │ │ CronEngine │ │ -│ │ │ │ │ │ │ │ │ │ per-item │ │ 응답 처리 │ │ tick │ │ │ │ Done/HITL │ │ timeout │ │ force_trigger│ │ │ │ 분류 │ │ terminal │ │ 품질 루프 │ │ │ └─────────────┘ └─────────────┘ └──────────────┘ │ -└───────────────────────────────────────────────────────────────────────┘ - │ - ┌────────────┼────────────┐ - ▼ ▼ ▼ -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ DataSource │ │ AgentRuntime │ │ SQLite DB │ -│ │ │ │ │ │ -│ 수집 │ │ LLM 실행 │ │ queue_items │ -│ 컨텍스트 │ │ 추상화 │ │ history │ -│ source_data │ │ │ │ transition_ │ -│ │ │ │ │ events │ -└──────────────┘ └──────────────┘ └──────────────┘ +└──────────────────────────────┬────────────────────────────────────────┘ + │ 트리거 (호출만, 실행 책임 없음) + ┌────────────────┼─────────────────┐ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────┐ ┌──────────────┐ +│ WorkspaceBinding │ │ AgentRuntime │ │ SQLite DB │ +│ │ │ │ │ │ +│ sources: │ │ LLM 실행 │ │ queue_items │ +│ [DataSource] │ │ 추상화 │ │ history │ +│ 수집/컨텍스트 │ │ │ │ transition_ │ +│ │ │ │ │ events │ +│ hook: │ │ │ │ │ +│ LifecycleHook │ │ │ │ │ +│ on_enter() │ │ │ │ │ +│ on_done() │ │ │ │ │ +│ on_fail() │ │ │ │ │ +│ on_escalation()│ │ │ │ │ +│ │ │ │ │ │ +│ Hook impl이 │ │ │ │ │ +│ 실행 책임 소유 │ │ │ │ │ +└──────────────────┘ └──────────────┘ └──────────────┘ ``` --- @@ -307,17 +303,17 @@ trait SimilarityJudge | 레이어 | 책임 | 토큰 | |--------|------|------| -| Daemon | Orchestrator — 모듈 조율 + cron 스케줄링 | 0 | +| Daemon | CPU — 상태 머신 순회 + hook 트리거 + cron 스케줄링 | 0 | | Advancer | Pending→Ready→Running 전이, dependency gate (DB), conflict 검출 | 0 | -| Executor | handler/on_enter/on_done/on_fail 실행, escalation 적용 | handler별 | +| Executor | handler 실행, escalation 결정, hook 트리거 | handler별 | | StagnationDetector | CompositeSimilarity로 유사도 판단, 4가지 패턴 탐지 | 0 | | LateralAnalyzer | 내장 페르소나로 대안 접근법 분석, lateral_plan 생성 | 분석 시 | | HitlService | HITL 응답 처리, timeout 만료, terminal action | 0 | | Evaluator | Completed → Done/HITL 분류 (per-item, CLI 도구 호출) | 분류 시 | | 인프라 | worktree 생성/정리, 플랫폼 추상화 (셸, IPC) | 0 | -| DataSource | 수집(collect) + 컨텍스트 조회(context, source_data) | 0 | +| DataSource | 수집(collect) + 컨텍스트 조회(context, source_data) — 읽기 | 0 | +| LifecycleHook | 상태 전이 반응 (on_enter/on_done/on_fail/on_escalation) — 쓰기 | 0 | | AgentRuntime | LLM 실행 추상화 | handler별 | -| on_done/on_fail script | 외부 시스템에 결과 반영 | 0 | | Agent | `belt agent` / `/agent` 대화형 에이전트 | 세션 시 | | Cron | 주기 작업, 품질 루프 | job별 | @@ -326,13 +322,14 @@ trait SimilarityJudge ## OCP 확장점 ``` -새 외부 시스템 = DataSource impl 추가 → 코어 변경 0 -새 LLM = AgentRuntime impl 추가 → 코어 변경 0 -새 파이프라인 단계 = workspace yaml 수정 → 코어 변경 0 -새 품질 검사 = Cron 등록 → 코어 변경 0 -새 OS/플랫폼 = ShellExecutor impl 추가 → 코어 변경 0 -새 DataSource 컨텍스트 = source_data 자유 스키마 → 코어 변경 0 -새 유사도 알고리즘 = SimilarityJudge impl 추가 → 코어 변경 0 +새 외부 시스템 = DataSource + LifecycleHook impl 추가 → 코어 변경 0 +새 LLM = AgentRuntime impl 추가 → 코어 변경 0 +새 파이프라인 단계 = workspace yaml 수정 → 코어 변경 0 +새 lifecycle 반응 = LifecycleHook impl 추가/변경 → 코어 변경 0 +새 품질 검사 = Cron 등록 → 코어 변경 0 +새 OS/플랫폼 = ShellExecutor impl 추가 → 코어 변경 0 +새 DataSource 컨텍스트 = source_data 자유 스키마 → 코어 변경 0 +새 유사도 알고리즘 = SimilarityJudge impl 추가 → 코어 변경 0 ``` --- @@ -343,7 +340,9 @@ trait SimilarityJudge |------|------| | [QueuePhase 상태 머신](./concerns/queue-state-machine.md) | 상태 전이, 전이 캡슐화, worktree 생명주기, on_fail 조건 | | [Daemon](./concerns/daemon.md) | 내부 모듈 구조, 실행 루프, dependency gate (DB), concurrency, graceful shutdown | +| [Evaluator](./concerns/evaluator.md) | Progressive Evaluation Pipeline, Evaluate before Execute, Stage trait | | [Stagnation Detection](./concerns/stagnation.md) | Composite Similarity, 4가지 패턴, Lateral Thinking (내장 페르소나) | +| [LifecycleHook](./concerns/lifecycle-hook.md) | 상태 전이 반응 trait, DataSource별 impl, workspace 바인딩, lazy 로딩 | | [DataSource](./concerns/datasource.md) | trait, context 스키마 (source_data), 워크플로우 yaml, escalation | | [AgentRuntime](./concerns/agent-runtime.md) | LLM 실행 추상화, RuntimeRegistry | | [Agent](./concerns/agent-workspace.md) | 대화형 에이전트, per-item evaluate, slash command | @@ -358,12 +357,14 @@ trait SimilarityJudge | 항목 | v5 | v6 | 이슈 | |------|-----|-----|------| +| Lifecycle 반응 | on_done/on_fail yaml script, Executor 직접 실행 | `LifecycleHook` trait, DataSource별 impl, workspace 바인딩 | 신규 | +| Daemon 역할 | yaml script 실행기 | 상태 머신 CPU — hook 트리거만, 실행 책임 없음 | 신규 | | Daemon 내부 | 단일 daemon.rs | Orchestrator + Advancer·Executor·HitlService 모듈 분리 | #717 | | Phase 전이 | `item.phase =` 직접 대입 | `QueueItem::transit()` 강제, phase `pub(crate)` | #718 | | ItemContext | `issue`/`pr` 필드 직접 | `source_data: serde_json::Value` 추가 (OCP) | #719 | | hitl_terminal_action | `Option` | `Option` (타입 안전) | #720 | | Dependency gate | in-memory queue | DB 조회 기반 (restart-safety) | #721 | -| Evaluate | workspace 배치 | per-work_id 판정 | #722 | +| Evaluate | cron job, workspace 배치 | Daemon tick 정규 단계, Progressive Pipeline (Mechanical→Semantic→Consensus), history-aware 사전 검증 | #722 | | 실패 대응 | failure_count → 단순 retry | Composite Similarity 패턴 감지 + Lateral Thinking 사고 전환 | #723 | --- diff --git a/spec/draft/README.md b/spec/draft/README.md index 3b43ee7..c94c5b5 100644 --- a/spec/draft/README.md +++ b/spec/draft/README.md @@ -6,7 +6,9 @@ ## 핵심 변경 (v5 → v6) -- **Stagnation Detection**: 실패 횟수가 아니라 실패 패턴(SPINNING, OSCILLATION)을 감지하여 escalation 가속 +- **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 @@ -26,11 +28,13 @@ |------|------| | [QueuePhase 상태 머신](./concerns/queue-state-machine.md) | 8개 phase 전이, **전이 캡슐화**, worktree 생명주기, on_fail 조건 | | [Daemon](./concerns/daemon.md) | **내부 모듈 구조**, 실행 루프, **DB dependency gate**, concurrency, graceful shutdown | -| [Stagnation Detection](./concerns/stagnation.md) | **v6 신규** — 4가지 정체 패턴, 해시 기반 탐지, escalation 가속 | +| [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) | 주기 실행 + **per-item evaluate** + 품질 루프 | +| [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) | 배포 전략 | @@ -44,14 +48,15 @@ |---|------|------| | 01 | [온보딩](./flows/01-setup.md) | workspace 등록 → 컨벤션 부트스트랩 | | 02 | [스펙 생명주기](./flows/02-spec-lifecycle.md) | 스펙 등록 → 이슈 분해 → 완료 판정 | -| 03 | [이슈 파이프라인](./flows/03-issue-pipeline.md) | handlers 실행 → **stagnation detection** → evaluate → on_done | -| 04 | [실패 복구와 HITL](./flows/04-failure-and-hitl.md) | **stagnation 가속** → escalation → on_fail → 사람 개입 | +| 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 | diff --git a/spec/draft/concerns/agent-workspace.md b/spec/draft/concerns/agent-workspace.md index 1382104..b0ecf7b 100644 --- a/spec/draft/concerns/agent-workspace.md +++ b/spec/draft/concerns/agent-workspace.md @@ -16,27 +16,24 @@ handler 전부 성공 → Completed │ ▼ -evaluate cron (force_trigger로 즉시 실행 가능): - for item in queue.get(Completed): - belt agent -p "아이템 {work_id}의 완료 여부를 판단해줘. - belt context {work_id} --json 으로 컨텍스트를 확인하고, - belt queue done {work_id} 또는 belt queue hitl {work_id} 를 실행해줘" +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로 컨텍스트 조회 후 판정 │ - │ LLM이 해당 아이템의 belt context로 컨텍스트 조회 후 CLI 도구로 결정: + ├── Done → hook.on_done() 트리거 + │ ├── hook 성공 → Done (worktree 정리) + │ └── hook 실패 → Failed (worktree 보존) │ - ├── belt queue done $WORK_ID - │ → Daemon이 on_done script 실행 - │ ├── script 성공 → Done (worktree 정리) - │ └── script 실패 → 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를 호출하여 상태를 전이한다 +- Evaluator는 Daemon tick의 정규 단계. 상세: [Evaluator](./evaluator.md) +- SemanticStage에서 LLM이 `belt queue done/hitl` CLI를 직접 호출하여 상태를 전이한다 - 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템 판정에 영향 없다 -- `batch_size`로 한 tick에서 처리할 최대 아이템 수를 제한한다 +- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비한다 --- @@ -187,14 +184,9 @@ v4 (15개) → v5 (3개): ### classify-policy.md 로딩 경로 및 해석 (R-CW-007) `classify-policy.md`는 LLM 에이전트가 큐 아이템을 Done / HITL로 분류할 때 -참조하는 정책 문서다. 두 가지 형태로 존재한다: - -| 파일 | 위치 | 소비자 | 용도 | -|------|------|--------|------| -| `classify-policy.md` | `.claude/rules/` 하위 | LLM agent (system prompt) | 자연어 분류 기준 | -| `classify-policy.yaml` | workspace root | daemon evaluator | machine-readable 라우팅 규칙 | +참조하는 자연어 정책 문서다. `.claude/rules/` 하위에 위치하며, system prompt에 주입된다. -#### 로딩 경로 (classify-policy.md) +#### 로딩 경로 `agent::resolve_rules_dir` 함수가 아래 우선순위로 **디렉토리**를 탐색한다. 첫 번째로 존재하는 디렉토리 안의 **모든 `.md` 파일**이 로드된다. @@ -231,14 +223,16 @@ 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은 내부적으로 각 Completed 아이템에 대해 `belt agent --workspace -p ""`를 호출한다. 이때: +Evaluator의 SemanticStage가 내부적으로 `belt agent -p`를 호출한다. 이때: - **per-item**: 각 아이템에 대해 개별 프롬프트 발행 (v6 #722) - LLM이 `belt context $WORK_ID`로 해당 아이템 정보를 조회 - 판단 후 `belt queue done/hitl` CLI를 직접 호출하여 상태 전이 - classify-policy.md의 state별 Done 조건이 판단 기준 -- 개별 실패는 해당 아이템만 영향, `batch_size`로 tick당 처리 수 제한 +- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비 + +상세: [Evaluator](./evaluator.md) --- diff --git a/spec/draft/concerns/cron-engine.md b/spec/draft/concerns/cron-engine.md index 281ba14..2c140b5 100644 --- a/spec/draft/concerns/cron-engine.md +++ b/spec/draft/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초 | 완료 아이템 분류 (**per-item**, Done or HITL) — `belt agent -p` | | gap-detection | 1시간 | 스펙-코드 대조, gap 발견 시 이슈 생성 | | knowledge-extract | 1시간 | merged PR 지식 추출 | @@ -70,45 +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 불필요). - ---- - -## 스크립트 구조 (v6 per-item evaluate, #722) - -evaluate cron은 **per-work_id 단위**로 각 Completed 아이템에 대해 개별 LLM 판정을 실행한다. - -```bash -#!/bin/bash -# Guard: Completed 상태 아이템 있을 때만 -ITEMS=$(belt queue list --workspace "$WORKSPACE" --phase completed --json) -COUNT=$(echo "$ITEMS" | jq 'length') -if [ "$COUNT" = "0" ]; then exit 0; fi - -# Per-item evaluate: 각 아이템에 대해 개별 판정 -echo "$ITEMS" | jq -r '.[].work_id' | while read WORK_ID; do - belt agent --workspace "$WORKSPACE" -p \ - "아이템 $WORK_ID 의 완료 여부를 판단해줘. - belt context $WORK_ID --json 으로 컨텍스트를 확인하고, - belt queue done $WORK_ID 또는 belt queue hitl $WORK_ID 를 실행해줘" -done -``` +- 동기적으로 실행하지 않는다. cron의 `last_run_at`을 리셋할 뿐. +- gap-detection 등 품질 루프 job에 사용. -> **v5 대비 변경**: 이전에는 workspace 단위로 단일 프롬프트를 발행했다. v6에서는 각 아이템에 대해 context를 포함한 개별 프롬프트를 발행하여 세밀한 판정이 가능하다. `batch_size`로 한 tick에서 처리할 최대 아이템 수를 제한한다. +> **evaluate는 v6에서 Daemon tick 정규 단계로 이동**. Completed 아이템은 Evaluator가 다음 tick에서 Progressive Pipeline으로 판정한다. 상세: [Evaluator](./evaluator.md) --- diff --git a/spec/draft/concerns/daemon.md b/spec/draft/concerns/daemon.md index 2fff0b7..3833678 100644 --- a/spec/draft/concerns/daemon.md +++ b/spec/draft/concerns/daemon.md @@ -1,7 +1,8 @@ -# Daemon — Orchestrator +# 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가 사고를 전환하여 재시도한다. --- @@ -11,25 +12,29 @@ ``` 1. 수집: DataSource.collect() → Pending에 넣기 2. 전이: Pending → Ready → Running (자동, concurrency 제한) -3. 실행: yaml에 정의된 prompt/script 호출 -4. 완료: handler 성공 → Completed 전이 -5. 분류: evaluate cron이 Completed → Done or HITL 판정 (per-item, 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은 실행 루프와 모듈 조율만 담당하는 Orchestrator이다. +Daemon은 상태 머신을 순회하며 전이를 결정하고 hook을 트리거하는 CPU이다. ``` -Daemon (Orchestrator) +Daemon (CPU) loop { collector.collect() advancer.advance() - executor.execute() + executor.execute() // handler 실행 + hook 트리거 cron_engine.tick() } ``` @@ -37,7 +42,7 @@ Daemon (Orchestrator) | 모듈 | 책임 | 소유하는 상태 | |------|------|-------------| | **Advancer** | Pending→Ready→Running 전이, dependency gate (DB), conflict 검출 | queue, ConcurrencyTracker | -| **Executor** | handler/lifecycle 실행, 실패 시 stagnation 분석 + lateral plan + escalation | ActionExecutor, StagnationDetector, LateralAnalyzer | +| **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 목록 | @@ -47,7 +52,13 @@ Daemon (Orchestrator) ``` Executor │ - ├── ActionExecutor handler/script 실행 + ├── ActionExecutor handler(prompt/script) 실행 + │ + ├── hook: &dyn LifecycleHook 상태 전이 시 트리거 (실행 책임은 Hook impl) + │ ├── on_enter() + │ ├── on_done() + │ ├── on_fail() + │ └── on_escalation() │ ├── StagnationDetector 실패 시 패턴 탐지 │ └── judge: Box @@ -103,23 +114,32 @@ Advancer는 `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. 판정 (Evaluator) — 실행보다 먼저 + // Completed 아이템을 비용 순으로 판정: Mechanical → Semantic → (Consensus) + // Ready 아이템 중 이전 기록으로 판정 가능한 것은 handler 실행 없이 판정 + evaluator.evaluate() - // 2. 자동 전이 (Advancer) + // 3. 자동 전이 (Advancer) advancer.advance_pending_to_ready() // spec dep gate (DB) advancer.advance_ready_to_running(limit) // queue dep gate (DB) + concurrency - // 3. 실행 (Executor) + // 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 (실패 시 handler 건너뛰고 실패 경로) - result = executor.run_actions(state.on_enter, WORK_ID=item.id, WORKTREE=worktree) + // on_enter hook 트리거 (실패 시 handler 건너뛰고 실패 경로) + result = hook.on_enter(&ctx) if result.failed: - executor.handle_failure(item, state) + executor.handle_failure(item, hook) continue // handlers 순차 실행 (lateral_plan 있으면 prompt에 주입) @@ -127,33 +147,26 @@ loop { result = executor.execute(action, WORK_ID=item.id, WORKTREE=worktree, lateral_plan=item.lateral_plan) if result.failed: - executor.handle_failure(item, state) + executor.handle_failure(item, hook) break else: item.transit(Completed) - force_trigger("evaluate") - // 4. cron tick + // 5. cron tick (품질 루프: gap-detection, knowledge-extract 등) cron_engine.tick() } ``` -### Executor.handle_failure — Stagnation + Lateral + Escalation +### Executor.handle_failure — Stagnation + Lateral + Hook 트리거 ``` -fn handle_failure(item, state): - // ① ExecutionHistory 구성 (outputs/errors 별도) - execution_history = ExecutionHistory { - outputs: db.recent_summaries(item.source_id, item.state, limit=N), - errors: db.recent_errors(item.source_id, item.state, limit=N), - drifts: db.recent_drift_scores(item.source_id, item.state), - } - - // ② Stagnation Detection (항상 실행) - detections = stagnation_detector.detect(execution_history) +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 생성 (패턴 감지 시) lateral_plan = None if active.is_not_empty() && lateral_config.enabled: tried = db.get_tried_personas(item.source_id, item.state) @@ -161,32 +174,31 @@ fn handle_failure(item, state): if persona.is_some(): lateral_plan = lateral_analyzer.analyze( detection=active[0], - history=execution_history, persona=persona, workspace=item.workspace_id, ) - // ④ transition_events에 stagnation 기록 + // ③ transition_events에 stagnation 기록 record_stagnation_event(item, detections, lateral_plan) - // ⑤ Escalation 적용 (failure_count 기반, 기존과 동일) + // ④ Escalation 결정 (failure_count 기반) failure_count = count_failures(item.source_id, item.state) escalation = lookup_escalation(failure_count) - match escalation: - retry: - // lateral_plan을 새 아이템에 전달 - new_item = create_retry_item(item, lateral_plan) - // on_fail 실행 안 함, worktree 보존 + // ⑤ 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) - retry_with_comment: - run_actions(state.on_fail, WORK_ID=item.id, WORKTREE=worktree) + // ⑥ 상태 전이 + match escalation: + retry | retry_with_comment: new_item = create_retry_item(item, lateral_plan) // worktree 보존 hitl: - run_actions(state.on_fail, WORK_ID=item.id, WORKTREE=worktree) - // lateral report (모든 시도 이력)를 hitl_notes에 첨부 lateral_report = build_lateral_report(item.source_id, item.state) create_hitl_event(item, reason, hitl_notes=lateral_report) // worktree 보존 @@ -240,21 +252,29 @@ dependency phase 확인은 **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 + +| hook | 트리거 시점 | 실행 책임 | +|------|-----------|----------| +| `on_enter` | Running 진입 후, handler 실행 전 | Hook impl | +| `on_done` | evaluate Done 판정 후 | Hook impl | +| `on_fail` | 실패 시 (retry 제외) | Hook impl | +| `on_escalation` | escalation 결정 후 | Hook impl | -lifecycle hook은 **script만 허용**. LLM 호출은 handler에서만. +Daemon은 hook을 트리거만 한다. hook이 실제로 무엇을 하는지 모른다. +상세: [LifecycleHook](./lifecycle-hook.md) --- @@ -282,10 +302,15 @@ SIGINT → on_shutdown: ## 수용 기준 +### Daemon = CPU + +- [ ] Daemon은 상태 머신 순회 + hook 트리거만 담당한다 +- [ ] 상태 전이 시 workspace의 LifecycleHook.on_*()을 트리거한다 +- [ ] hook의 실행 결과(Result)만 받고, 구체적 동작을 모른다 + ### 내부 모듈 구조 (#717) -- [ ] Daemon 구조체는 실행 루프와 모듈 조율만 담당한다 -- [ ] phase 전이는 Advancer, handler 실행+stagnation+lateral+escalation은 Executor, HITL은 HitlService +- [ ] phase 전이는 Advancer, handler 실행+hook 트리거+stagnation+lateral은 Executor, HITL은 HitlService - [ ] 각 모듈은 독립적으로 단위 테스트 가능하다 - [ ] 모듈 간 의존은 trait 또는 함수 파라미터로만 전달 (순환 참조 금지) @@ -322,8 +347,9 @@ SIGINT → on_shutdown: ### 관련 문서 - [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) — 워크플로우 정의 + context 스키마 +- [DataSource](./datasource.md) — 수집/컨텍스트 추상화 - [AgentRuntime](./agent-runtime.md) — LLM 실행 추상화 - [Cron 엔진](./cron-engine.md) — evaluate cron + 품질 루프 diff --git a/spec/draft/concerns/data-model.md b/spec/draft/concerns/data-model.md index 9ff3ada..b8846c0 100644 --- a/spec/draft/concerns/data-model.md +++ b/spec/draft/concerns/data-model.md @@ -1,6 +1,6 @@ # Data Model -> 관련 문서: [DESIGN-v6](../DESIGN-v6.md), [QueuePhase 상태 머신](./queue-state-machine.md), [DataSource](./datasource.md), [Cron 엔진](./cron-engine.md), [Stagnation](./stagnation.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`)에 저장된다. 이 문서는 테이블 스키마, 도메인 모델, 직렬화 규칙을 한 곳에 정의한다. @@ -91,7 +91,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' | 'stagnation' + event_type TEXT NOT NULL, -- 'phase_enter' | 'handler' | 'evaluate' | 'hook' | 'stagnation' phase TEXT, -- 진입한 phase from_phase TEXT, -- 이전 phase detail TEXT, -- 사람이 읽을 수 있는 설명 @@ -99,7 +99,7 @@ CREATE TABLE transition_events ( ); ``` -**v6 변경 (#723)**: `event_type`에 `'stagnation'` 추가. `detail`에 탐지 패턴, confidence, evidence, 가속 결과를 JSON으로 기록한다. +**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 @@ -231,9 +231,9 @@ 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) @@ -270,13 +270,15 @@ HITL 생성 경로. 직렬화 시 snake_case. 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` 필드 타입으로도 사용된다. @@ -344,20 +346,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 } diff --git a/spec/draft/concerns/datasource.md b/spec/draft/concerns/datasource.md index 04092f1..c05d0e8 100644 --- a/spec/draft/concerns/datasource.md +++ b/spec/draft/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,9 @@ 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`에 자유 스키마로 채운다. @@ -251,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 '.source_data.issue.title // .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 '.source_data.issue.number // .issue.number') - REPO=$(echo $CTX | jq -r '.source.url') - gh issue comment $ISSUE --body "구현 실패" -R $REPO -``` +상세: [LifecycleHook](./lifecycle-hook.md) -실행 주체: Daemon의 Executor 모듈이 상태 전이 시점에 직접 실행. -- `on_enter`: Running 진입 후, handler 실행 전. **실패 시 handler를 실행하지 않고 즉시 escalation 정책을 적용**한다. 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 결정 후 | — | --- @@ -302,7 +286,7 @@ escalation: # 선택지: skip (종료) 또는 replan (스펙 수정 제안) ``` -> **v6 (#723) Stagnation 가속**: failure_count 기반 escalation에 stagnation detection 결과가 가속 요소로 적용된다. 예: 1회 실패인데 SPINNING이 감지되면 retry → retry_with_comment로 승격. OSCILLATION이면 → hitl로 즉시 승격. 상세: [Stagnation Detection](./stagnation.md) +> **v6 Stagnation과 Escalation의 관계**: escalation은 failure_count 기반으로 결정되고, stagnation은 lateral_plan 주입에 집중한다. 두 관심사는 직교한다 — escalation이 "언제 멈출지"를 결정하고, stagnation이 "다르게 시도할지"를 결정한다. escalation 발생 시 `LifecycleHook.on_escalation()`이 DataSource별 반응을 처리한다. 상세: [LifecycleHook](./lifecycle-hook.md) ### on_fail 실행 조건 @@ -345,8 +329,9 @@ queue_items 테이블: ### 관련 문서 - [DESIGN-v6](../DESIGN-v6.md) — 전체 아키텍처 +- [LifecycleHook](./lifecycle-hook.md) — 상태 전이 반응 trait - [AgentRuntime](./agent-runtime.md) — handler prompt 실행 -- [Stagnation Detection](./stagnation.md) — escalation 가속 +- [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/draft/concerns/evaluator.md new file mode 100644 index 0000000..7dd7009 --- /dev/null +++ b/spec/draft/concerns/evaluator.md @@ -0,0 +1,272 @@ +# Evaluator — Progressive Evaluation Pipeline + +> 실행 전에 판정하고, 필요할 때만 실행한다. +> 비용이 낮은 검증부터 단계적으로 수행하여 불필요한 LLM 호출을 줄인다. +> Ouroboros의 3-stage progressive evaluation을 차용. + +--- + +## 핵심 원칙 + +``` +1. Evaluate before Execute — 실행보다 판정이 먼저 +2. Cheapest first — 비용 0 검증 → LLM 1회 → (다중 LLM) +3. History-aware — 이전 기록으로 판정 가능하면 handler 실행 생략 +``` + +--- + +## Daemon tick에서의 위치 + +``` +loop { + collect() // 수집 + evaluator.evaluate() // 판정 — 실행보다 먼저 + advancer.advance() // 전이 + executor.execute() // 실행 + cron_engine.tick() // 품질 루프 +} +``` + +Evaluator는 두 가지를 처리한다: + +1. **Completed 아이템 판정** — handler가 끝난 아이템을 Done/HITL로 분류 +2. **Ready 아이템 사전 검증** — 이전 기록 기반으로 handler 실행 없이 판정 가능한지 확인 + +``` +evaluator.evaluate(): + // 1. Completed 아이템 → 단계적 판정 + for item in queue.get(Completed): + decision = pipeline.evaluate(item) + match decision: + Done → hook.on_done(), transit(Done) + Hitl → create_hitl_event() + NeedMoreWork → transit(Ready) // 재실행 필요 + + // 2. Ready 아이템 → 사전 검증 (이력 기반) + for item in queue.get(Ready): + if can_judge_from_history(item): + // handler 실행 없이 판정 (비용 0) + decision = pipeline.evaluate_from_history(item) + match decision: + Done → hook.on_done(), transit(Done) + Hitl → create_hitl_event() + Inconclusive → pass // Advancer가 Running으로 전이 +``` + +--- + +## Progressive Evaluation Pipeline + +비용이 낮은 단계부터 순차 실행. 앞 단계에서 판정되면 뒤 단계를 건너뛴다. + +``` +┌─ Stage 1: Mechanical (비용 $0) ─────────────────────────────┐ +│ │ +│ cargo test, cargo clippy, lint 등 결정적 검증 │ +│ → 실패 시 즉시 retry (LLM 안 부름) │ +│ → 성공 시 Stage 2로 │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─ Stage 2: Semantic (LLM 1회) ────────────────────────────────┐ +│ │ +│ LLM이 "이 결과가 충분한가?" 판정 │ +│ classify-policy.md 기준으로 Done / HITL 분류 │ +│ → 확실하면 판정 완료 │ +│ → 불확실하면 Stage 3로 (Phase 2) │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ (Phase 2) +┌─ Stage 3: Consensus (다중 LLM) ─────────────────────────────┐ +│ │ +│ 트리거 조건 충족 시에만 실행 │ +│ 다중 모델 투표 또는 역할 기반 토론 │ +│ → 최종 판정 │ +└──────────────────────────────────────────────────────────────┘ +``` + +--- + +## trait 정의 + +```rust +/// 평가 단계 하나를 추상화. +/// 각 impl이 자기 방식으로 판정하고, 다음 단계로 넘길지 결정. +#[async_trait] +pub trait EvaluationStage: Send + Sync { + fn name(&self) -> &str; + + /// 판정 수행. 확정이면 Done/Hitl, 불확실하면 Inconclusive. + async fn evaluate(&self, ctx: &EvalContext, db: &dyn Db) -> Result; +} + +pub enum EvalDecision { + Done, // 충분 — hook.on_done() 트리거 + Hitl { reason: String }, // 사람 필요 — HITL 이벤트 생성 + Retry, // Stage 1 실패 — handler 재실행 + Inconclusive, // 이 단계에서 판정 불가 — 다음 단계로 +} +``` + +--- + +## EvaluationPipeline — Stage 컴포지트 + +```rust +pub struct EvaluationPipeline { + stages: Vec>, +} + +impl EvaluationPipeline { + /// 등록된 Stage를 순차 실행. 확정 판정이 나오면 중단. + pub async fn evaluate(&self, ctx: &EvalContext, db: &dyn Db) -> Result { + for stage in &self.stages { + match stage.evaluate(ctx, db).await? { + EvalDecision::Inconclusive => continue, // 다음 Stage로 + decision => return Ok(decision), // 확정 — 중단 + } + } + // 모든 Stage가 Inconclusive → HITL로 에스컬레이션 + Ok(EvalDecision::Hitl { reason: "all stages inconclusive".into() }) + } +} +``` + +v6에서는 `MechanicalStage` + `SemanticStage`만 등록. Phase 2에서 `ConsensusStage`를 추가하면 코어 변경 0. + +--- + +## Stage 상세 + +### Stage 1: MechanicalStage (v6) + +worktree에서 결정적 검증을 실행한다. LLM 비용 0. + +```rust +struct MechanicalStage { + commands: Vec, // workspace yaml에서 로드 +} + +impl EvaluationStage for MechanicalStage { + async fn evaluate(&self, ctx: &EvalContext, db: &dyn Db) -> Result { + for cmd in &self.commands { + let result = execute_in_worktree(cmd, &ctx.worktree); + if result.failed { + return Ok(EvalDecision::Retry); // 빌드/테스트 실패 → 재시도 + } + } + Ok(EvalDecision::Inconclusive) // 기계적으로는 통과 → Semantic으로 + } +} +``` + +workspace yaml에서 검증 커맨드를 정의: + +```yaml +evaluate: + mechanical: + - "cargo test" + - "cargo clippy -- -D warnings" +``` + +### Stage 2: SemanticStage (v6) + +LLM이 classify-policy.md 기준으로 Done/HITL 판정. 현재 evaluate 로직과 동일. + +```rust +struct SemanticStage { + runtime: Box, +} + +impl EvaluationStage for SemanticStage { + async fn evaluate(&self, ctx: &EvalContext, db: &dyn Db) -> Result { + // belt agent -p 호출 (기존과 동일) + // LLM이 belt queue done/hitl CLI를 호출하여 판정 + } +} +``` + +### Stage 3: ConsensusStage (Phase 2) + +다중 LLM 투표. 트리거 조건 충족 시에만 실행. + +트리거 조건 (Ouroboros 차용): +- drift score가 임계값 초과 +- lateral thinking이 적용된 retry +- uncertainty가 높은 경우 + +--- + +## History-aware 사전 검증 + +Ready 아이템에 대해 이전 기록을 조회하여 handler 실행 없이 판정 가능한지 확인한다. + +``` +can_judge_from_history(item): + // 같은 source_id로 이전에 동일한 state를 성공한 기록이 있는가? + prev = db.query("SELECT * FROM history + WHERE source_id=? AND state=? AND status='done' + ORDER BY created_at DESC LIMIT 1", item.source_id, item.state) + + if prev.is_some(): + // 이전 성공 결과와 현재 worktree 상태를 비교 + similarity = judge.score(prev.summary, current_state) + if similarity >= threshold: + return true // 이전 결과로 판정 가능 + + return false // handler 실행 필요 +``` + +--- + +## Evaluator 모듈 — Daemon 내부 + +``` +Daemon (CPU) + ├── Evaluator ← tick 루프에서 실행보다 먼저 + │ └── EvaluationPipeline + │ ├── MechanicalStage (v6) + │ ├── SemanticStage (v6) + │ └── ConsensusStage (Phase 2) + │ + ├── Advancer + ├── Executor + ├── HitlService + └── CronEngine +``` + +Evaluator는 cron job이 아닌 **Daemon tick 루프의 정규 단계**이다. 실행(Executor)보다 먼저 동작하여, 판정 가능한 아이템은 handler 실행 없이 처리한다. + +--- + +## 영향 범위 + +| 변경 | 내용 | +|------|------| +| Daemon tick 순서 | evaluate → advance → execute → cron | +| Evaluator 위치 | cron job → Daemon 모듈 | +| CronEngine | evaluate 제거, 품질 루프(gap-detection 등)만 담당 | +| workspace yaml | `evaluate.mechanical` 섹션 추가 (검증 커맨드) | + +--- + +## 수용 기준 + +- [ ] Evaluator가 Daemon tick에서 Executor보다 먼저 실행된다 +- [ ] EvaluationPipeline이 Stage를 비용 순으로 순차 실행한다 +- [ ] MechanicalStage가 worktree에서 결정적 검증을 수행한다 (비용 0) +- [ ] SemanticStage가 LLM으로 Done/HITL 판정한다 +- [ ] Stage가 확정 판정을 내리면 후속 Stage를 건너뛴다 +- [ ] Ready 아이템에 대해 이전 기록 기반 사전 검증이 동작한다 +- [ ] 새 EvaluationStage impl 추가 시 코어 변경 없다 (OCP) + +--- + +### 관련 문서 + +- [DESIGN-v6](../DESIGN-v6.md) — Daemon tick 순서 +- [Daemon](./daemon.md) — 실행 루프 +- [Agent Workspace](./agent-workspace.md) — classify-policy.md (SemanticStage 기준) +- [Stagnation Detection](./stagnation.md) — PatternDetector (유사도 판단 재사용) +- [LifecycleHook](./lifecycle-hook.md) — hook.on_done() 트리거 diff --git a/spec/draft/concerns/lifecycle-hook.md b/spec/draft/concerns/lifecycle-hook.md new file mode 100644 index 0000000..977413d --- /dev/null +++ b/spec/draft/concerns/lifecycle-hook.md @@ -0,0 +1,401 @@ +# LifecycleHook — 상태 전이 반응 추상화 + +> Daemon이 상태를 전이할 때, 해당 workspace의 LifecycleHook이 외부 시스템에 반응한다. +> handler(prompt/script)는 "무엇을 실행할지", hook은 "상태가 바뀌었을 때 어떻게 반응할지". +> 새 DataSource 유형 추가 = LifecycleHook impl 추가, 코어 변경 0 (OCP). + +--- + +## 문제 — 현재 구조 + +현재 on_done/on_fail/on_enter는 yaml `Vec`으로 정의되어 Daemon(Executor)이 직접 실행한다. + +``` +현재: + StateConfig { + handlers: Vec, // 작업 + on_enter: Vec, // lifecycle — yaml script + on_done: Vec, // lifecycle — yaml script + on_fail: Vec, // lifecycle — yaml script + } + + Executor가 상태 전이 시 on_* script를 직접 subprocess로 실행 +``` + +문제점: +1. **handler와 hook이 같은 곳(yaml)에 혼재** — 성격이 다른 관심사가 구분되지 않음 +2. **DataSource별 반응을 정의할 수 없음** — GitHub/Jira/Slack 모두 bash script로만 표현 +3. **on_escalation이 없음** — escalation 발생 시 외부 시스템 반응 경로가 없음 +4. **Daemon이 도메인을 침범** — Executor가 `gh issue comment` 같은 script를 직접 실행 + +--- + +## 설계 — handler와 hook 분리 + +### 핵심 구분 + +| | handler | hook | +|---|---------|------| +| **역할** | 작업 자체 (분석, 구현, 리뷰) | 상태 변화에 대한 외부 시스템 반응 | +| **정의** | workspace yaml (prompt/script) | LifecycleHook trait impl | +| **실행 주체** | Daemon Executor | Daemon이 트리거, Hook impl이 실행 | +| **소유** | workspace yaml | DataSource 유형 + workspace 설정 | +| **예시** | "이슈를 구현해줘" | PR 생성, 라벨 변경, 코멘트 작성 | + +### Daemon = CPU 비유 + +``` +Daemon (CPU) — tick마다: + │ + ├── 큐 스캔 → 아이템 상태 확인 + │ + ├── 상태 전이 결정 (Advancer) + │ + ├── handler 실행 (Executor) — yaml prompt/script + │ + └── 전이 발생 시 hook 트리거 + → workspace에 바인딩된 LifecycleHook.on_*() 호출 + → Hook impl이 자기 시스템에 맞게 반응 +``` + +--- + +## trait 정의 + +```rust +/// 상태 전이 시 외부 시스템에 반응하는 lifecycle hook. +/// +/// DataSource 유형별로 impl을 제공하고, workspace별로 인스턴스가 생성된다. +/// Daemon은 상태 전이 시점에 해당 hook을 트리거할 뿐, 구체적 반응을 모른다. +#[async_trait] +pub trait LifecycleHook: Send + Sync { + /// Running 진입 후, handler 실행 전. + /// 실패 시 handler를 건너뛰고 escalation 경로로 진입. + async fn on_enter(&self, ctx: &HookContext) -> Result<()>; + + /// evaluate가 Done 판정 후 호출. + /// 실패 시 Failed 상태로 전이. + async fn on_done(&self, ctx: &HookContext) -> Result<()>; + + /// handler 또는 on_enter 실패 시 호출. + /// escalation level에 따라 조건부 (retry에서는 호출 안 함). + async fn on_fail(&self, ctx: &HookContext) -> Result<()>; + + /// escalation 결정 후 호출. + /// DataSource가 자기 시스템에 맞는 반응 수행. + async fn on_escalation(&self, ctx: &HookContext, action: EscalationAction) -> Result<()>; +} + +/// Hook에 전달되는 컨텍스트. +/// belt context CLI와 동일한 정보를 구조체로 제공. +pub struct HookContext { + pub work_id: String, + pub worktree: PathBuf, + pub item: QueueItem, + pub item_context: ItemContext, // DataSource.get_context() 결과 + pub failure_count: u32, +} +``` + +--- + +## DataSource와 LifecycleHook 관계 + +``` +DataSource (trait) LifecycleHook (trait) + │ │ + ├── collect() ├── on_enter() + ├── get_context() ├── on_done() + │ ├── on_fail() + │ └── on_escalation() + │ + │ 수집 + 컨텍스트 상태 전이 반응 + │ (읽기) (쓰기/반영) + +GitHubSource ◄──────────────► GitHubLifecycleHook +JiraSource ◄──────────────► JiraLifecycleHook +SlackSource ◄──────────────► SlackLifecycleHook +``` + +분리 이유: +- **단일 책임**: DataSource는 읽기(수집/조회), Hook은 쓰기(반영) +- **독립 테스트**: Hook만 mock하여 상태 전이 테스트 가능 +- **조합 가능**: 같은 DataSource에 다른 Hook 전략을 조합할 수 있음 + +--- + +## workspace 바인딩 + +LifecycleHook은 workspace마다 인스턴스가 생성된다. 같은 GitHub DataSource라도 workspace별로 다른 hook 동작이 가능하다. + +```rust +/// workspace 등록 시 생성되는 바인딩. +struct WorkspaceBinding { + config: WorkspaceConfig, + sources: Vec>, + hook: Box, // 이 workspace의 lifecycle +} +``` + +### hook 동작은 어디서 정의하는가 + +workspace yaml에서 hook 설정을 정의하고, DataSource 유형에 맞는 Hook impl이 설정을 해석한다. + +```yaml +# workspace.yaml +name: auth-project +sources: + github: + url: https://github.com/org/repo + states: + implement: + trigger: { label: "belt:implement" } + handlers: + - prompt: "이슈를 구현해줘" # handler — 작업 + +# hook 동작은 DataSource 유형(github)이 결정 +# GitHubLifecycleHook이 sources.github 설정을 기반으로 동작: +# on_done → PR 생성, 라벨 전환 (belt:implement → belt:review) +# on_fail → 이슈에 실패 코멘트 +# on_escalation(hitl) → 이슈에 라벨 추가 (belt:needs-human) +``` + +### yaml에서 hook 커스터마이징 + +DataSource 유형의 기본 동작 위에 workspace별 오버라이드가 가능하다. + +```yaml +sources: + github: + url: https://github.com/org/repo + hooks: # 선택적 오버라이드 + on_done: + label_remove: "belt:implement" + label_add: "belt:review" + create_pr: true + on_fail: + comment: true # 실패 코멘트 작성 여부 + on_escalation: + hitl_label: "needs-human" # HITL 시 추가할 라벨 +``` + +yaml `hooks` 섹션이 없으면 DataSource 유형의 기본 동작을 사용한다. + +--- + +## Daemon 실행 루프 변경 + +``` +loop { + // 1. 수집 + for binding in workspaces: + for source in binding.sources: + items = source.collect() + queue.push(Pending, items) + + // 2. 전이 (Advancer) — 변경 없음 + advancer.advance_pending_to_ready() + advancer.advance_ready_to_running(limit) + + // 3. 실행 (Executor) + for item in queue.get_new(Running): + binding = lookup_workspace_binding(item) + hook = binding.hook + + // on_enter hook + result = hook.on_enter(&ctx) + if result.failed: + executor.handle_failure(item, hook) + continue + + // handlers 순차 실행 — yaml prompt/script (변경 없음) + for action in state.handlers: + result = executor.execute(action, ...) + if result.failed: + executor.handle_failure(item, hook) + break + else: + item.transit(Completed) + force_trigger("evaluate") + + // 4. cron tick — 변경 없음 + cron_engine.tick() +} + +fn handle_failure(item, hook): + // stagnation detection + lateral plan (변경 없음) + ... + + // escalation 결정 (failure_count 기반, 변경 없음) + escalation = lookup_escalation(failure_count) + + // hook 트리거 + hook.on_escalation(&ctx, escalation) + + // on_fail 조건부 호출 (retry 제외) + if escalation.should_run_on_fail(): + hook.on_fail(&ctx) + + match escalation: + retry → create_retry_item(item, lateral_plan) + retry_with_comment → create_retry_item(item, lateral_plan) + hitl → create_hitl_event(item, ...) +``` + +--- + +## 기존 yaml on_done/on_fail/on_enter 마이그레이션 + +### Phase 1 (v6): 호환 유지 + +`ScriptLifecycleHook` — 기존 yaml script를 LifecycleHook trait으로 감싸는 어댑터. + +```rust +/// 기존 yaml script 기반 hook을 LifecycleHook trait으로 감싸는 어댑터. +/// v6에서 기존 workspace yaml과의 호환성을 유지한다. +struct ScriptLifecycleHook { + state_configs: HashMap, +} + +#[async_trait] +impl LifecycleHook for ScriptLifecycleHook { + async fn on_done(&self, ctx: &HookContext) -> Result<()> { + let state = &self.state_configs[&ctx.item.state]; + for script in &state.on_done { + execute_script(&script.script, &ctx.work_id, &ctx.worktree)?; + } + Ok(()) + } + + async fn on_escalation(&self, _ctx: &HookContext, _action: EscalationAction) -> Result<()> { + Ok(()) // 기존 yaml에는 on_escalation 개념 없음 — no-op + } + // ... +} +``` + +### Phase 2 (v7+): DataSource별 전용 Hook + +```rust +struct GitHubLifecycleHook { + config: GitHubHookConfig, // yaml hooks 섹션에서 파싱 +} + +#[async_trait] +impl LifecycleHook for GitHubLifecycleHook { + async fn on_done(&self, ctx: &HookContext) -> Result<()> { + // PR 생성, 라벨 전환 등 — GitHub API/CLI 직접 사용 + } + + async fn on_escalation(&self, ctx: &HookContext, action: EscalationAction) -> Result<()> { + match action { + EscalationAction::Hitl => { + // 이슈에 needs-human 라벨 추가 + // lateral report를 코멘트로 작성 + } + EscalationAction::RetryWithComment => { + // 이슈에 실패 코멘트 작성 + } + _ => {} + } + Ok(()) + } +} +``` + +--- + +## StateConfig 변경 + +```rust +// v5 (현재) +pub struct StateConfig { + pub trigger: TriggerConfig, + pub handlers: Vec, + pub on_enter: Vec, // lifecycle — yaml에 혼재 + pub on_done: Vec, + pub on_fail: Vec, +} + +// v6 (Phase 1 — 호환 유지) +pub struct StateConfig { + pub trigger: TriggerConfig, + pub handlers: Vec, // handler만 남음 + // on_enter/on_done/on_fail은 ScriptLifecycleHook 어댑터가 처리 + // 기존 yaml 호환을 위해 파싱은 유지하되, StateConfig에서 LifecycleHook 생성 시 소비 + #[serde(default)] + pub on_enter: Vec, + #[serde(default)] + pub on_done: Vec, + #[serde(default)] + pub on_fail: Vec, +} + +// v7+ (Phase 2 — 완전 분리) +pub struct StateConfig { + pub trigger: TriggerConfig, + pub handlers: Vec, // handler만 +} +// on_*/hooks는 SourceConfig.hooks 또는 DataSource별 Hook impl로 이동 +``` + +--- + +## Lazy 로딩 — 메모리 최적화 + +Daemon 시작 시 모든 workspace의 script/설정을 메모리에 올리지 않는다. Hook impl이 실행 책임을 가지므로, 트리거 시점에 필요한 설정만 lazy하게 로드한다. + +``` +현재 (v5): + Daemon 시작 + → 모든 workspace yaml 파싱 + → 모든 StateConfig (handlers + on_done + on_fail + on_enter) 메모리 상주 + → workspace N개 × state M개 × script K개 = 전부 적재 + +제안 (v6): + Daemon 시작 + → workspace 메타정보만 적재 (name, sources, escalation) + → LifecycleHook은 trait object로 바인딩만 (설정 미로드) + + tick에서 hook 트리거 시: + → hook.on_done(&ctx) 호출 + → Hook impl이 해당 시점에 필요한 설정/스크립트를 로드 + → 실행 후 해제 +``` + +workspace가 늘어나도 Daemon의 메모리 부담이 선형 증가하지 않는다. Hook impl이 캐싱 전략을 자체 결정할 수 있다. + +--- + +## 영향 범위 + +| 문서/모듈 | 변경 내용 | +|-----------|----------| +| `belt-core/source.rs` | DataSource trait 변경 없음 | +| `belt-core/` (신규) | `LifecycleHook` trait + `HookContext` 정의 | +| `belt-daemon/daemon.rs` | Executor가 hook.on_*() 트리거로 변경 | +| `datasource.md` | hook 분리 반영, on_done/on_fail yaml 정의 → trait 위임 | +| `daemon.md` | Executor 실행 루프에서 hook 트리거 반영 | +| `DESIGN-v6.md` | OCP 확장점에 LifecycleHook 추가 | + +--- + +## 수용 기준 + +- [ ] `LifecycleHook` trait이 belt-core에 정의된다 +- [ ] Daemon Executor는 상태 전이 시 hook.on_*()을 트리거한다 +- [ ] hook.on_*()의 구체적 동작은 DataSource 유형별 impl이 결정한다 +- [ ] LifecycleHook은 workspace별로 인스턴스가 생성된다 +- [ ] `ScriptLifecycleHook` 어댑터가 기존 yaml script와 호환을 유지한다 +- [ ] on_escalation이 escalation 발생 시 호출된다 +- [ ] 새 DataSource 유형 추가 시 코어 변경 없이 Hook impl만 추가하면 된다 +- [ ] handler(prompt/script)와 hook(lifecycle 반응)이 명확히 분리된다 + +--- + +### 관련 문서 + +- [DESIGN-v6](../DESIGN-v6.md) — 설계 철학 (Daemon = Orchestrator) +- [DataSource](./datasource.md) — 수집/컨텍스트 추상화 +- [Daemon](./daemon.md) — 실행 루프, Executor 모듈 +- [QueuePhase 상태 머신](./queue-state-machine.md) — 상태 전이 규칙 diff --git a/spec/draft/concerns/queue-state-machine.md b/spec/draft/concerns/queue-state-machine.md index f32a809..e1ddade 100644 --- a/spec/draft/concerns/queue-state-machine.md +++ b/spec/draft/concerns/queue-state-machine.md @@ -216,7 +216,7 @@ for item in queue.get(Completed): ``` - 개별 판정 실패 시 해당 아이템만 Completed에 머물고, 다른 아이템 판정에 영향 없다 -- `batch_size`로 한 tick에서 처리할 최대 아이템 수를 제한한다 +- evaluate LLM 호출도 `daemon.max_concurrent` slot을 소비한다 — 별도 batch 제어 없음 - 기존 `eval_failure_counts`는 이미 per-work_id로 관리됨 (설계 의도 일치) ### 실패 원칙 @@ -279,8 +279,9 @@ Completed는 **안전한 대기 상태**. evaluate가 실패하든 CLI가 실패 - [DESIGN-v6](../DESIGN-v6.md) — 설계 철학 - [Daemon](./daemon.md) — 내부 모듈 구조 + 실행 루프 -- [Stagnation Detection](./stagnation.md) — 반복 패턴 감지 + escalation 가속 -- [DataSource](./datasource.md) — escalation 정책 + on_fail script +- [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/draft/concerns/stagnation.md index ce2eaf5..da72c11 100644 --- a/spec/draft/concerns/stagnation.md +++ b/spec/draft/concerns/stagnation.md @@ -68,21 +68,29 @@ pub struct StagnationDetection { } ``` -### ExecutionHistory +### PatternDetector trait -outputs와 errors를 **별도 필드**로 분리한다. Ouroboros와 동일하게 각 데이터 소스를 독립적으로 검사. +각 패턴 탐지기가 DB에서 자기 관심사에 맞는 데이터를 직접 조회한다. 중간 구조체(`ExecutionHistory`) 없이, 각 detector가 필요한 쿼리만 수행. ```rust -pub struct ExecutionHistory { - pub outputs: Vec, // handler 출력 (history.summary) - pub errors: Vec, // 에러 메시지 (history.error) — 별도 검사 - pub drift_scores: Vec, // 진행 점수 (optional) +/// 정체 패턴을 탐지하는 단일 알고리즘. +/// 각 impl이 DB에서 자기 관심사에 맞는 데이터를 직접 조회한다. +#[async_trait] +pub trait PatternDetector: Send + Sync { + fn pattern(&self) -> StagnationPattern; + async fn detect(&self, source_id: &str, state: &str, db: &dyn Db) -> Result; } ``` -- `outputs`: history 테이블의 `summary` 필드. 최근 N개 sliding window. -- `errors`: history 테이블의 `error` 필드. 별도로 sliding window. -- `drift_scores`: 정량적 진행 지표. Phase 2에서 도입. +| Detector | DB 조회 | 구현 시점 | +|----------|---------|----------| +| `SpinningDetector` | `history.summary`, `history.error` (최근 N개) | v6 | +| `OscillationDetector` | `history.summary` (최근 2N개) | v6 | +| `DriftDetector` | `history.summary` + `source_data` (목표 vs 결과) | Phase 2 | +| `DiminishingDetector` | drift score 이력 | Phase 2 | + +- v6: SPINNING + OSCILLATION — 텍스트 유사도 기반, DB에 데이터 이미 있음 +- Phase 2: NO_DRIFT + DIMINISHING — drift score 산출 파이프라인 구축 후 detector impl 추가, 코어 변경 0 --- @@ -209,36 +217,45 @@ impl SimilarityJudge for Ncd { --- -## StagnationDetector +## StagnationDetector — PatternDetector 컴포지트 ```rust pub struct StagnationDetector { - pub judge: Box, // Composite 또는 단일 Judge + pub detectors: Vec>, pub config: StagnationConfig, } impl StagnationDetector { - pub fn detect(&self, history: &ExecutionHistory) -> Vec; + /// 등록된 모든 PatternDetector를 실행하고 결과를 합산. + pub async fn detect(&self, source_id: &str, state: &str, db: &dyn Db) + -> Result>; } ``` -Detector는 `SimilarityJudge` trait 하나만 의존. Composite인지 단일 Judge인지 모른다. +v6에서는 `SpinningDetector` + `OscillationDetector`만 등록. Phase 2에서 `DriftDetector` + `DiminishingDetector`를 추가하면 코어 변경 없이 동작한다. + +각 PatternDetector는 내부적으로 `SimilarityJudge`를 사용할 수 있다 (SPINNING, OSCILLATION). drift 기반 detector는 SimilarityJudge 불필요. ### 탐지 알고리즘 -#### SPINNING — 유사 출력 반복 +#### SpinningDetector (v6) — 유사 출력 반복 -outputs와 errors를 **별도로** 검사한다. 어느 쪽이든 감지되면 SPINNING. +DB에서 summaries와 errors를 **별도로** 조회·검사한다. 어느 쪽이든 감지되면 SPINNING. ``` -detect_spinning(history): - // 1. outputs 검사 - recent_outputs = history.outputs[-threshold..] +SpinningDetector.detect(source_id, state, db): + // 1. outputs 검사 — DB에서 직접 조회 + recent_outputs = db.query("SELECT summary FROM history + WHERE source_id=? AND state=? ORDER BY created_at DESC LIMIT ?", + source_id, state, spinning_threshold) if all_pairs_similar(recent_outputs, judge, similarity_threshold): return SPINNING(source: "outputs") - // 2. errors 검사 - recent_errors = history.errors[-threshold..] + // 2. errors 검사 — DB에서 직접 조회 + recent_errors = db.query("SELECT error FROM history + WHERE source_id=? AND state=? AND error IS NOT NULL + ORDER BY created_at DESC LIMIT ?", + source_id, state, spinning_threshold) if all_pairs_similar(recent_errors, judge, similarity_threshold): return SPINNING(source: "errors") @@ -253,11 +270,13 @@ all_pairs_similar(items, judge, threshold): - 유사도 기준: `similarity_threshold` (기본 0.8) - confidence: 유사도 점수의 평균값 -#### OSCILLATION — 교대 반복 +#### OscillationDetector (v6) — 교대 반복 ``` -detect_oscillation(history): - recent = history.outputs[-(cycles * 2)..] +OscillationDetector.detect(source_id, state, db): + recent = db.query("SELECT summary FROM history + WHERE source_id=? AND state=? ORDER BY created_at DESC LIMIT ?", + source_id, state, cycles * 2) // 짝수 그룹 내 유사 even_similar = all_pairs_similar(recent[::2], judge, similarity_threshold) @@ -270,25 +289,31 @@ detect_oscillation(history): return OSCILLATION ``` -#### NO_DRIFT / DIMINISHING_RETURNS — 수치 기반 +#### DriftDetector / DiminishingDetector (Phase 2) — drift score 기반 -drift score 기반. SimilarityJudge 불필요, 기존 알고리즘 유지. +Phase 2에서 `PatternDetector` impl로 추가. SimilarityJudge 불필요. +DB에서 작업 결과(history.summary)와 원래 목표(source_data)를 조회하여 goal_drift를 산출한다. Ouroboros의 가중 합산 모델을 차용: + +``` +combined_drift = (goal_drift × 0.5) + (constraint_drift × 0.3) + (ontology_drift × 0.2) ``` -detect_no_drift(history): - scores = history.drift_scores[-iterations..] - deltas = consecutive_abs_deltas(scores) - if all(d < epsilon for d in deltas): - confidence = 1.0 - (avg(deltas) / epsilon) - return NO_DRIFT -detect_diminishing(history): - scores = history.drift_scores[-(iterations+1)..] - improvements = consecutive_diffs(scores) // lower = better - if all(imp < threshold for imp in improvements): - is_decreasing = monotonically_decreasing(improvements) - confidence = if is_decreasing { 0.8 } else { 0.6 } - return DIMINISHING_RETURNS +- **goal_drift**: 원래 목표(이슈 본문) vs 현재 결과(summary)의 Jaccard 거리 +- **constraint_drift**: 제약 위반 추적 (workspace별 이력 필요) +- **ontology_drift**: 개념 공간 변화 (workspace별 이력 필요) + +v6에서는 trait 경계만 정의하고 impl 없음 — `StagnationDetector`에 등록되지 않으므로 동작하지 않는다. Phase 2에서 impl 추가 시 코어 변경 0. + +``` +DriftDetector.detect(source_id, state, db): + summaries = db.query("SELECT summary FROM history WHERE ...") + source_data = db.query("SELECT source_data FROM queue_items WHERE ...") + goal = extract_goal(source_data) // 이슈 본문 등 + drift = compute_goal_drift(goal, summaries.last()) + store_drift_score(db, source_id, state, drift) + // NO_DRIFT: 최근 N개 drift 변화량 < epsilon + // DIMINISHING: 개선폭이 감소 추세 ``` --- @@ -438,15 +463,16 @@ handler/on_enter 실행 실패 persona, lateral_plan, judge_scores } ``` -### 데이터 소스 +### 데이터 소스 — 각 Detector가 DB에서 직접 조회 -| 데이터 | 출처 | 용도 | -|--------|------|------| -| handler 출력 | `history.summary` | SPINNING, OSCILLATION (outputs) | -| 에러 메시지 | `history.error` | SPINNING (errors, 별도 검사) | -| 시도 번호 | `history.attempt` | sliding window 범위 결정 | -| drift score | (Phase 2) `belt context` 확장 | NO_DRIFT, DIMINISHING_RETURNS | -| 이전 lateral | `transition_events` (stagnation) | 페르소나 중복 제외 | +| 데이터 | DB 테이블/컬럼 | Detector | 구현 시점 | +|--------|---------------|----------|----------| +| handler 출력 | `history.summary` | SpinningDetector, OscillationDetector | v6 | +| 에러 메시지 | `history.error` | SpinningDetector (별도 검사) | v6 | +| 시도 번호 | `history.attempt` | sliding window 범위 결정 | v6 | +| 원래 목표 | `queue_items.source_data` | DriftDetector (goal_drift 산출) | Phase 2 | +| drift score 이력 | (Phase 2 테이블) | DiminishingDetector | Phase 2 | +| 이전 lateral | `transition_events` (stagnation) | 페르소나 중복 제외 | v6 | ### 이벤트 기록 diff --git a/spec/draft/flows/03-issue-pipeline.md b/spec/draft/flows/03-issue-pipeline.md index bd413ef..4ee603a 100644 --- a/spec/draft/flows/03-issue-pipeline.md +++ b/spec/draft/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,10 +35,10 @@ DataSource.collect(): trigger 조건 매칭 (예: belt:analyze 라벨) ├── 전부 성공 → Completed │ │ │ ▼ - │ evaluate cron (per-item, force_trigger로 즉시 실행 + 주기 폴링 하이브리드): + │ evaluate (per-item, concurrency slot 소비): │ "이 아이템의 결과가 충분한가?" - │ ├── Done → on_done script 실행 → worktree 정리 - │ │ └── script 실패 → Failed (로그 기록, 재시도 가능) + │ ├── Done → hook.on_done() 트리거 → worktree 정리 + │ │ └── hook 실패 → Failed (로그 기록) │ └── HITL → HITL 이벤트 생성 → 사람 대기 (worktree 보존) │ └── 실패 (handler 또는 on_enter) diff --git a/spec/draft/flows/04-failure-and-hitl.md b/spec/draft/flows/04-failure-and-hitl.md index e73849f..5d7f018 100644 --- a/spec/draft/flows/04-failure-and-hitl.md +++ b/spec/draft/flows/04-failure-and-hitl.md @@ -38,14 +38,15 @@ Stagnation Analyzer (항상 실행): ▼ ④ Escalation (failure_count 기반, lateral_plan 포함): │ - ├── retry → lateral_plan 주입, 재시도 (on_fail 안 함, worktree 보존) - ├── retry_with_comment → lateral_plan 주입, on_fail script 실행, 재시도 (worktree 보존) - └── hitl → lateral_report 첨부, on_fail script 실행, HITL 이벤트 (worktree 보존) + ├── 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을 실행하지 않는다. "조용한 재시도"로 외부 시스템에 노이즈를 주지 않는다. +`retry`만 on_fail hook을 트리거하지 않는다. "조용한 재시도"로 외부 시스템에 노이즈를 주지 않는다. +Daemon은 hook을 트리거만 하고, 실행 책임은 workspace의 LifecycleHook impl이 가진다. --- From 7e86e05eac0436eb888e2e39203efef537f79767 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: Mon, 6 Apr 2026 22:23:19 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(spec):=20resolve=20remaining=20review?= =?UTF-8?q?=20items=20=E2=80=94=20HITL=20UX,=20error=20scenarios,=20L2/L3?= =?UTF-8?q?=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves High 6, Medium 8-10 from spec review: - High 6: Add QueuePhase ↔ history.status mapping table to data-model.md - Medium 8 (L2/L3 boundary): Remove algorithm details from flow docs (CompositeSimilarity, ExecutionHistory, SQL DDL). Keep user-facing descriptions, delegate internals to concern docs. - Medium 9 (HITL UX): Replace fixed 4-choice enum with LLM-driven contextual questions. LLM analyzes situation (lateral report, history, HITL trigger) and generates recommended actions. User responds in natural language, LLM translates to system action. - Medium 10 (Error scenarios): - flow-01: CLI immediate validation with specific error messages - flow-02: New Analyzing state — LLM analyzes spec quality before Active transition. Configurable via ~/.belt/config.yaml - flow-03: Circuit breaker for infra errors (GitHub API, worktree). Distinct from stagnation (handler logic failure). Source-level isolation with backoff retry and dashboard visibility. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/draft/concerns/data-model.md | 13 +++ spec/draft/flows/01-setup.md | 27 +++++ spec/draft/flows/02-spec-lifecycle.md | 60 ++++++++--- spec/draft/flows/03-issue-pipeline.md | 59 +++++++++-- spec/draft/flows/04-failure-and-hitl.md | 133 ++++++++++++++---------- spec/draft/flows/05-monitoring.md | 30 +----- 6 files changed, 216 insertions(+), 106 deletions(-) diff --git a/spec/draft/concerns/data-model.md b/spec/draft/concerns/data-model.md index b8846c0..eb34c1d 100644 --- a/spec/draft/concerns/data-model.md +++ b/spec/draft/concerns/data-model.md @@ -77,6 +77,19 @@ 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 = ?` diff --git a/spec/draft/flows/01-setup.md b/spec/draft/flows/01-setup.md index 2257e2f..ec7e651 100644 --- a/spec/draft/flows/01-setup.md +++ b/spec/draft/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/draft/flows/02-spec-lifecycle.md b/spec/draft/flows/02-spec-lifecycle.md index 60025f2..347f99d 100644 --- a/spec/draft/flows/02-spec-lifecycle.md +++ b/spec/draft/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/draft/flows/03-issue-pipeline.md b/spec/draft/flows/03-issue-pipeline.md index 4ee603a..9d6844a 100644 --- a/spec/draft/flows/03-issue-pipeline.md +++ b/spec/draft/flows/03-issue-pipeline.md @@ -44,18 +44,18 @@ DataSource.collect(): trigger 조건 매칭 (예: belt:analyze 라벨) └── 실패 (handler 또는 on_enter) │ ▼ - Stagnation Analyzer (항상 실행): - CompositeSimilarity로 outputs/errors 유사도 분석 - 패턴 감지 시 → LateralAnalyzer가 페르소나로 대안 분석 - → lateral_plan 생성 + Stagnation 분석 (항상 실행): + 이전 시도와 유사한 패턴인가? (반복, 교대 등) + 패턴 감지 시 → 다른 접근법으로 전환하여 재시도 │ ▼ - Escalation (failure_count 기반, lateral_plan 포함): - ├── retry → lateral_plan 주입, 재시도, worktree 보존 - ├── retry_with_comment → lateral_plan 주입, on_fail + 재시도 - └── hitl → lateral_report 첨부, on_fail + HITL 생성 - └── 사람: done/retry/skip/replan - └── timeout → terminal (skip 또는 replan) + Escalation (실패 횟수 기반): + ├── 1회 → 조용히 재시도 (대안 접근법 주입) + ├── 2회 → 외부 시스템에 알림 + 재시도 + └── 3회 → 사람에게 전달 (lateral report 첨부) + └── 사람: done/retry/skip/replan + └── timeout → terminal (skip 또는 replan) + 상세: [실패 복구와 HITL](./04-failure-and-hitl.md) ``` --- @@ -102,9 +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 정책 - [Stagnation Detection](../concerns/stagnation.md) — 실패 패턴 감지 -- [Cron 엔진](../concerns/cron-engine.md) — evaluate cron + 품질 루프 +- [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 index 5d7f018..7f9e098 100644 --- a/spec/draft/flows/04-failure-and-hitl.md +++ b/spec/draft/flows/04-failure-and-hitl.md @@ -7,36 +7,27 @@ ## 실패 경로 ``` -handler 또는 on_enter 실행 실패 +handler 또는 hook.on_enter() 실패 │ ▼ -Stagnation Analyzer (항상 실행): - ① ExecutionHistory 구성 - outputs = DB history.summary (별도) - errors = DB history.error (별도) +Stagnation 분석 (항상 실행): + 이전 시도 기록을 분석하여 정체 패턴을 감지: │ - ▼ - ② StagnationDetector.detect - CompositeSimilarity (ExactHash + TokenFingerprint + NCD) - outputs → SPINNING? OSCILLATION? - errors → SPINNING? (별도 검사) - drifts → NO_DRIFT? DIMINISHING? + ├── 같은 실패 반복 (SPINNING) + ├── A↔B 교대 반복 (OSCILLATION) + └── (Phase 2) 진행 정체, 개선폭 감소 │ - ├── 패턴 없음 ────────────────────── escalation 적용 (lateral 없이) + ├── 패턴 없음 ─────── escalation 적용 (기존 방식으로 재시도) │ └── 패턴 감지 ─┐ ▼ - ③ LateralAnalyzer - 패턴 → 페르소나 선택 (이전 시도 제외) - SPINNING → HACKER - OSCILLATION → ARCHITECT - NO_DRIFT → RESEARCHER - DIMINISHING → SIMPLIFIER - 복합 → CONTRARIAN - belt agent -p → lateral_plan 생성 + 사고 전환 (Lateral Thinking): + 패턴에 맞는 다른 접근법을 선택 (이전 시도와 중복 없이) + 예: 반복 실패 → 워크어라운드 시도, 교대 반복 → 구조 재설계 + → 대안 접근 계획(lateral plan) 생성 │ ▼ - ④ Escalation (failure_count 기반, lateral_plan 포함): + Escalation (실패 횟수 기반, lateral plan 포함): │ ├── retry → hook.on_escalation(retry), lateral_plan 주입, 재시도 (worktree 보존) ├── retry_with_comment → hook.on_escalation + hook.on_fail, lateral_plan 주입, 재시도 @@ -130,52 +121,85 @@ on_fail: | 스펙 완료 | 모든 linked issues Done → 최종 확인 요청 | | 충돌 | DependencyGuard가 스펙 충돌 감지 | -### HITL에 lateral report 첨부 +### HITL 진입 — LLM이 질문을 구성 + +HITL에 진입하면 LLM이 상황(lateral report, 이력, HITL 경로)을 분석하여 **맥락에 맞는 질문과 추천 선택지**를 구성한다. 고정된 4개 선택지가 아니라, 상황별로 다른 제안이 나온다. + +#### 예시: Escalation HITL (handler 3회 실패) + +``` +"JWT middleware 구현이 3회 실패했습니다. -hitl에 도달하면 지금까지의 모든 lateral 시도 이력이 `hitl_notes`에 첨부된다: + 시도 이력: + 1회: compile error (Session not found) + 2회: tower-sessions 시도 → 다른 에러 발생 + 3회: trait object 시도 → 컴파일 성공, 테스트 실패 + 추천: + 1. axum-sessions crate로 전환하여 재시도 + 2. Session 관련 코드를 별도 이슈로 분리 + 3. 현재 결과로 PR 생성 (부분 완료) + 4. 이 이슈 건너뛰기 + 또는 직접 지시를 입력하세요" ``` -HITL Event: - work_id: github:org/repo#42:implement - reason: retry_max_exceeded - hitl_notes: - "Stagnation Report: - pattern: SPINNING (3회 유사 에러 — CompositeSimilarity score: 0.92) - attempt 1: compile error (Session not found) - attempt 2: HACKER 제안 → tower-sessions 시도 → 다른 에러 발생 - attempt 3: CONTRARIAN 제안 → trait object 시도 → 컴파일 성공, 테스트 실패 +#### 예시: Evaluate HITL (완료 여부 불확실) - 2회 접근 전환 후에도 미해결. 구조적 문제일 가능성." ``` +"JWT middleware 구현 결과를 검토했으나 확신이 부족합니다. + + 현재 상태: + - 컴파일 성공, 테스트 18/20 통과 + - 실패 테스트: session expiry, concurrent access -사람이 lateral report를 참고하여 더 정확한 판단을 내릴 수 있다. + 추천: + 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 요청 (사람 응답 또는 hitl timeout) +replan (사용자 응답 또는 hitl timeout) → replan_count 증가 (max 3) → max 초과 시: Skipped (worktree 정리) → max 이내: - 1. HitlReason::SpecModificationProposed 이벤트 생성 - 2. Failed 전이 (worktree 보존) - 3. Agent가 실패 컨텍스트 + lateral report를 분석하여 스펙 수정 제안 - 4. 사용자가 /spec update로 스펙 수정 → 새 이슈 생성 → 파이프라인 재진입 + 1. Agent가 실패 컨텍스트 + lateral report를 분석하여 스펙 수정 제안 + 2. 사용자가 /spec update로 스펙 수정 → 새 이슈 생성 → 파이프라인 재진입 ``` ### 타임아웃 @@ -188,8 +212,6 @@ replan 요청 (사람 응답 또는 hitl timeout) replan → replan 처리 (위 참조) ``` -`hitl_terminal_action`은 `EscalationAction` enum 타입 (#720). - --- ## Graceful Shutdown @@ -205,9 +227,10 @@ SIGINT → on_shutdown: ### 관련 문서 -- [Stagnation Detection](../concerns/stagnation.md) — Composite Similarity + Lateral Thinking -- [Daemon](../concerns/daemon.md) — Executor.handle_failure 상세 -- [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, Persona enum +- [Data Model](../concerns/data-model.md) — HitlReason, EscalationAction enum diff --git a/spec/draft/flows/05-monitoring.md b/spec/draft/flows/05-monitoring.md index cbe509a..4cad28f 100644 --- a/spec/draft/flows/05-monitoring.md +++ b/spec/draft/flows/05-monitoring.md @@ -207,39 +207,15 @@ 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에 진입한 경우 `HitlReason::StagnationDetected`와 함께 lateral report(시도한 페르소나, 각 분석 결과)가 표시되어 사용자가 지금까지의 접근 전환 이력을 참고할 수 있다. +> **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, stagnation - phase TEXT, -- Pending, Ready, Running, Completed, Done, HITL, Failed, Skipped - detail TEXT, -- script exit code, prompt result, error message, stagnation evidence - 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) ---