Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions spec/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,21 @@ workspace.concurrency (workspace yaml 루트) + daemon.max_concurrent 2단계. e
└──────────────────────────────────────────────┘
```

### Tick 순서와 Evaluator 위치

```
매 tick:
① collect — DataSource에서 새 아이템 수집 → Pending
② evaluate — Completed 아이템을 Done/HITL로 판정 (concurrency slot 해제)
③ advance — Pending→Ready→Running 전이 (slot 확보)
④ execute — Running 아이템의 handler 실행 + hook 트리거
⑤ cron.tick — 품질 루프 (gap-detection 등)
```

> **Evaluate before Execute**: Evaluator가 Executor보다 먼저 동작한다.
> 이전 tick에서 Completed된 아이템이 현재 tick에서 판정(Done/HITL)되어 concurrency slot이 해제된 후,
> Advancer가 새 아이템을 Running으로 전이시킨다. 이 순서가 뒤바뀌면 slot 부족으로 불필요한 대기가 발생한다.

### 상태별 소유 모듈

| Phase | 소유 모듈 | 핵심 동작 | Hook 트리거 |
Expand Down
8 changes: 4 additions & 4 deletions spec/concerns/agent-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ v4 (15개) → v5 (3개):
첫 번째로 존재하는 디렉토리 안의 **모든 `.md` 파일**이 로드된다.

```
Priority 1: claw_config.rules_path (workspace YAML 명시)
Priority 2: $BELT_HOME/workspaces/<name>/claw/system/ (per-workspace)
Priority 3: $BELT_HOME/claw-workspace/.claude/rules/ (global, belt claw init)
Priority 1: agent_config.rules_path (workspace YAML 명시)
Priority 2: $BELT_HOME/workspaces/<name>/agent/system/ (per-workspace)
Priority 3: $BELT_HOME/agent-workspace/.claude/rules/ (global, belt agent init)
```

`$BELT_HOME`은 환경변수 `BELT_HOME`이 설정되지 않으면 `~/.belt`로 기본값.
Expand All @@ -208,7 +208,7 @@ Priority 3: $BELT_HOME/claw-workspace/.claude/rules/ (global, belt claw init)
#### 구현 위치

- `crates/belt-cli/src/agent.rs` — `resolve_rules_dir`, `load_rules_from_dir`
- `crates/belt-cli/src/claw/mod.rs` — `ClawWorkspace::init`, `default_classify_policy()`
- `crates/belt-cli/src/agent/workspace.rs` — `AgentWorkspace::init`, `default_classify_policy()`

### LLM이 사용 가능한 도구

Expand Down
19 changes: 17 additions & 2 deletions spec/concerns/daemon.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,22 @@ dependency phase 확인은 **DB 조회 기반**:
3. 판정:
- Done → gate open
- DB에 없음 → gate open (orphan 허용)
- 그 외 → gate blocked
- Failed / Skipped → gate blocked (영구 차단 — 수동 해제 필요)
- HITL → gate blocked (사람 응답 대기)
- 그 외 (Pending / Ready / Running / Completed) → gate blocked (진행 대기)
```

#### 에지 케이스

| 케이스 | 정책 | 이유 |
|--------|------|------|
| **순환 의존** | 등록 시점에 탐지 → 거부 | 런타임에 교착 발생하면 복구 불가 |
| **orphan** (DB에 없음) | gate open | 삭제된 아이템에 의존하면 영원히 차단됨 |
| **dependency가 Failed/Skipped** | gate blocked | 전제 조건 미충족 — 운영자가 dependency를 해결하거나 의존 관계를 제거해야 함 |
| **자기 자신에 의존** | 등록 시점에 거부 | 순환의 특수 케이스 |

순환 탐지는 `queue_dependencies` 테이블에 INSERT 시 DFS로 사이클을 확인한다. 사이클이 발견되면 등록을 거부하고 에러를 반환한다.

### Conflict Gate

`check_conflict_gate()` — entry_point 겹침 감지. DB 기반.
Expand Down Expand Up @@ -328,7 +341,9 @@ SIGINT → on_shutdown:

- [ ] queue dependency의 phase 확인은 DB 조회 기준이다
- [ ] 재시작 후에도 dependency gate가 정확히 동작한다
- [ ] dependency가 Failed/Hitl이면 blocked, DB에 없으면 open
- [ ] dependency가 Failed/Skipped이면 blocked, DB에 없으면 open
- [ ] 순환 의존은 등록 시점에 탐지하여 거부한다
- [ ] 자기 자신에 대한 의존은 거부한다

### Concurrency

Expand Down
8 changes: 4 additions & 4 deletions spec/concerns/datasource.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,9 @@ Escalation level은 **순차 실행 구간**과 **대안 선택 구간**으로

```yaml
escalation:
1: retry # 같은 state에서 재시도 (on_fail 실행 안 함)
2: retry_with_comment # on_fail script 실행 + 재시도
3: hitl # on_fail script 실행 + HITL 이벤트 생성
1: retry # 같은 state에서 재시도 (hook.on_fail 트리거 안 함)
2: retry_with_comment # hook.on_fail 트리거 + 재시도
3: hitl # hook.on_fail 트리거 + HITL 이벤트 생성
terminal: skip # hitl에서 사람이 결정하지 않으면 (timeout) 적용되는 최종 액션
# 선택지: skip (종료) 또는 replan (스펙 수정 제안)
```
Expand All @@ -290,7 +290,7 @@ escalation:

### on_fail 실행 조건

`retry`만 on_fail script를 실행하지 않는다. 나머지(`retry_with_comment`, `hitl`)는 on_fail script 실행 후 해당 액션을 수행한다.
`retry`만 hook.on_fail()을 트리거하지 않는다. 나머지(`retry_with_comment`, `hitl`)는 hook.on_fail() 트리거 후 해당 액션을 수행한다.

```
1회 실패 → retry → 조용히 재시도 (worktree 보존)
Expand Down
6 changes: 4 additions & 2 deletions spec/concerns/evaluator.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl EvaluationPipeline {
}
```

v6에서는 `MechanicalStage` + `SemanticStage`만 등록. Phase 2에서 `ConsensusStage`를 추가하면 코어 변경 0.
**v6 범위**: `MechanicalStage` + `SemanticStage`만 등록. Phase 2(v7+)에서 `ConsensusStage`를 추가하면 코어 변경 0 (OCP).

---

Expand Down Expand Up @@ -187,7 +187,9 @@ impl EvaluationStage for SemanticStage {
}
```

### Stage 3: ConsensusStage (Phase 2)
### Stage 3: ConsensusStage (Phase 2, v7+)

> **v6 범위 아님** — trait 경계만 정의. v6에서는 Stage 1·2만 등록된다.

다중 LLM 투표. 트리거 조건 충족 시에만 실행.

Expand Down
95 changes: 76 additions & 19 deletions spec/concerns/lifecycle-hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,71 @@ sources:

yaml `hooks` 섹션이 없으면 DataSource 유형의 기본 동작을 사용한다.

### Hook impl 동적 로딩

Hook impl은 Daemon 시작 시 일괄 생성하지 않는다. hook 트리거 시점에 DB에서 workspace 정보를 조회하고, 필요한 Hook impl을 동적으로 로드한다.

```
hook 트리거 시점 (on_enter, on_done, on_fail, on_escalation):
1. DB에서 workspace 조회 (config_path)
2. yaml 파싱 → sources 키에서 DataSource 유형 식별
3. DataSource 유형 → Hook impl 생성:
github → GitHubLifecycleHook (Phase 2)
jira → JiraLifecycleHook (v7+)
...
Phase 1 fallback: 매핑이 없거나 yaml에 on_done/on_fail script가 존재하면
→ ScriptLifecycleHook 어댑터 사용
4. yaml의 hooks 섹션으로 오버라이드 적용 (없으면 기본값)
5. hook.on_*() 실행
```

동적 로딩의 이점:
- **실행 중 workspace 추가**: Daemon 재시작 없이 `belt workspace add`로 등록하면 다음 tick부터 동작
- **설정 변경 즉시 반영**: yaml 수정 시 다음 hook 트리거에서 최신 설정 반영
- **메모리 절약**: 사용하지 않는 workspace의 Hook impl을 메모리에 유지하지 않음

사용자가 직접 Hook 유형을 지정할 필요 없다 — DB에 기록된 workspace의 DataSource 유형이 곧 Hook 유형을 결정한다.

---

## Hook 에러 처리 정책

| hook | 실패 시 | 이유 |
|------|---------|------|
| `on_enter()` | handler 건너뛰고 escalation 경로 | 전제 조건 미충족 — handler 실행 의미 없음 |
| `on_done()` | Failed 상태로 전이 | 외부 반영 실패 — 수동 확인 필요 |
| `on_fail()` | 로그 기록, 상태 전이에 영향 없음 | 이미 실패 경로 — 2차 실패로 흐름 중단하지 않음 |
| `on_escalation()` | 로그 기록, escalation 진행 | 알림 실패가 escalation 자체를 막으면 안 됨 |

```
원칙:
- on_enter/on_done 실패 → 상태 전이에 영향 (치명적)
- on_fail/on_escalation 실패 → 로그만 기록 (비치명적)
- 모든 hook 실패는 transition_events에 event_type='hook_error'로 기록
```

### on_escalation과 on_fail 호출 순서

escalation 발생 시 두 hook이 순차 호출된다:

```
executor.handle_failure(item, hook):
escalation = lookup_escalation(failure_count)

// ① on_escalation — 항상 호출 (모든 escalation 액션에 대해)
hook.on_escalation(&ctx, escalation)

// ② on_fail — 조건부 호출 (retry 제외)
if escalation.should_run_on_fail(): // retry_with_comment, hitl
hook.on_fail(&ctx)

// ③ 상태 전이
match escalation: ...
```

> on_escalation은 escalation 유형(retry/hitl/...)을 `action` 파라미터로 받아 유형별 반응이 가능하다.
> on_fail은 "실패했다"는 사실만 전달한다. retry에서는 "조용한 재시도"이므로 호출하지 않는다.

---

## Daemon 실행 루프 변경
Expand Down Expand Up @@ -343,29 +408,17 @@ pub struct StateConfig {

---

## Lazy 로딩 — 메모리 최적화
## 동적 로딩과 메모리

Daemon 시작 시 모든 workspace의 script/설정을 메모리에 올리지 않는다. Hook impl이 실행 책임을 가지므로, 트리거 시점에 필요한 설정만 lazy하게 로드한다.
Hook impl은 트리거 시점에 생성되고, 실행 후 해제된다. Daemon은 workspace 메타정보(DB)만 유지한다.

```
현재 (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이 해당 시점에 필요한 설정/스크립트를 로드
→ 실행 후 해제
v5: Daemon 시작 → 모든 yaml 파싱 → 전체 StateConfig 메모리 상주
v6: Daemon 시작 → DB에서 workspace 목록만 조회
hook 트리거 시 → DB → yaml 파싱 → Hook impl 생성 → 실행 → 해제
```

workspace가 늘어나도 Daemon의 메모리 부담이 선형 증가하지 않는다. Hook impl이 캐싱 전략을 자체 결정할 수 있다.
workspace가 늘어나도 Daemon의 메모리 부담이 선형 증가하지 않는다. 자주 트리거되는 workspace의 Hook impl은 LRU 캐시로 재사용하고, yaml 변경 시(`updated_at` 비교) 캐시를 무효화한다.

---

Expand All @@ -389,7 +442,11 @@ workspace가 늘어나도 Daemon의 메모리 부담이 선형 증가하지 않
- [ ] hook.on_*()의 구체적 동작은 DataSource 유형별 impl이 결정한다
- [ ] LifecycleHook은 workspace별로 인스턴스가 생성된다
- [ ] `ScriptLifecycleHook` 어댑터가 기존 yaml script와 호환을 유지한다
- [ ] on_escalation이 escalation 발생 시 호출된다
- [ ] on_escalation이 escalation 발생 시 항상 호출되고, on_fail은 retry를 제외하고 호출된다
- [ ] on_escalation → on_fail 순서로 호출된다
- [ ] on_enter/on_done 실패 시 상태 전이에 영향을 준다 (escalation / Failed)
- [ ] on_fail/on_escalation 실패 시 로그만 기록하고 흐름을 중단하지 않는다
- [ ] 모든 hook 실패는 transition_events에 기록된다
- [ ] 새 DataSource 유형 추가 시 코어 변경 없이 Hook impl만 추가하면 된다
- [ ] handler(prompt/script)와 hook(lifecycle 반응)이 명확히 분리된다

Expand Down
6 changes: 4 additions & 2 deletions spec/concerns/stagnation.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ impl StagnationDetector {
}
```

v6에서는 `SpinningDetector` + `OscillationDetector`만 등록. Phase 2에서 `DriftDetector` + `DiminishingDetector`를 추가하면 코어 변경 없이 동작한다.
**v6 범위**: `SpinningDetector` + `OscillationDetector`만 등록. Phase 2(v7+)에서 `DriftDetector` + `DiminishingDetector`를 추가하면 코어 변경 없이 동작한다 (OCP).

각 PatternDetector는 내부적으로 `SimilarityJudge`를 사용할 수 있다 (SPINNING, OSCILLATION). drift 기반 detector는 SimilarityJudge 불필요.

Expand Down Expand Up @@ -289,7 +289,9 @@ OscillationDetector.detect(source_id, state, db):
return OSCILLATION
```

#### DriftDetector / DiminishingDetector (Phase 2) — drift score 기반
#### DriftDetector / DiminishingDetector (Phase 2, v7+) — drift score 기반

> **v6 범위 아님** — trait 경계만 정의. v6에서는 SpinningDetector·OscillationDetector만 등록된다.

Phase 2에서 `PatternDetector` impl로 추가. SimilarityJudge 불필요.

Expand Down
Loading