From 24f13eba337e9a3a317ce811d527ace796c4b95b 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: Wed, 8 Apr 2026 10:14:44 +0900 Subject: [PATCH 1/2] docs(spec): add Evaluator tick order to DESIGN and unify v6 terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DESIGN.md: add "Tick 순서와 Evaluator 위치" section between flow diagram and module table, explaining why evaluate-before-execute matters for concurrency slot release - datasource.md: replace v5 "on_fail script" with v6 "hook.on_fail()" (3 places) - evaluator.md, stagnation.md: add "v6 범위 아님" markers to Phase 2 features (ConsensusStage, DriftDetector/DiminishingDetector) and clarify v6 scope Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/DESIGN.md | 15 +++++++++++++++ spec/concerns/datasource.md | 8 ++++---- spec/concerns/evaluator.md | 6 ++++-- spec/concerns/stagnation.md | 6 ++++-- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/spec/DESIGN.md b/spec/DESIGN.md index 70815dc..472db66 100644 --- a/spec/DESIGN.md +++ b/spec/DESIGN.md @@ -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 트리거 | diff --git a/spec/concerns/datasource.md b/spec/concerns/datasource.md index c05d0e8..e83cf0c 100644 --- a/spec/concerns/datasource.md +++ b/spec/concerns/datasource.md @@ -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 (스펙 수정 제안) ``` @@ -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 보존) diff --git a/spec/concerns/evaluator.md b/spec/concerns/evaluator.md index 7dd7009..2794c4f 100644 --- a/spec/concerns/evaluator.md +++ b/spec/concerns/evaluator.md @@ -133,7 +133,7 @@ impl EvaluationPipeline { } ``` -v6에서는 `MechanicalStage` + `SemanticStage`만 등록. Phase 2에서 `ConsensusStage`를 추가하면 코어 변경 0. +**v6 범위**: `MechanicalStage` + `SemanticStage`만 등록. Phase 2(v7+)에서 `ConsensusStage`를 추가하면 코어 변경 0 (OCP). --- @@ -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 투표. 트리거 조건 충족 시에만 실행. diff --git a/spec/concerns/stagnation.md b/spec/concerns/stagnation.md index da72c11..6231784 100644 --- a/spec/concerns/stagnation.md +++ b/spec/concerns/stagnation.md @@ -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 불필요. @@ -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 불필요. From 54a2d1b62bee350de482240fdca7ea8b0af9dc64 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: Thu, 9 Apr 2026 09:55:06 +0900 Subject: [PATCH 2/2] docs(spec): strengthen lifecycle-hook, dependency gate, and agent naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lifecycle-hook.md: add dynamic Hook impl loading (DB→yaml→create→execute→release with LRU cache), hook error handling policy (critical vs non-critical), on_escalation→on_fail call ordering with pseudocode - daemon.md: detail dependency gate edge cases (cycle detection via DFS at registration, orphan→open, Failed/Skipped→blocked, self-dependency→reject) - agent-workspace.md: fix stale claw→agent naming in rules path priority and implementation references Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/concerns/agent-workspace.md | 8 +-- spec/concerns/daemon.md | 19 ++++++- spec/concerns/lifecycle-hook.md | 95 +++++++++++++++++++++++++------- 3 files changed, 97 insertions(+), 25 deletions(-) diff --git a/spec/concerns/agent-workspace.md b/spec/concerns/agent-workspace.md index d449d70..7098113 100644 --- a/spec/concerns/agent-workspace.md +++ b/spec/concerns/agent-workspace.md @@ -192,9 +192,9 @@ v4 (15개) → v5 (3개): 첫 번째로 존재하는 디렉토리 안의 **모든 `.md` 파일**이 로드된다. ``` -Priority 1: claw_config.rules_path (workspace YAML 명시) -Priority 2: $BELT_HOME/workspaces//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//agent/system/ (per-workspace) +Priority 3: $BELT_HOME/agent-workspace/.claude/rules/ (global, belt agent init) ``` `$BELT_HOME`은 환경변수 `BELT_HOME`이 설정되지 않으면 `~/.belt`로 기본값. @@ -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이 사용 가능한 도구 diff --git a/spec/concerns/daemon.md b/spec/concerns/daemon.md index e46402d..41516b0 100644 --- a/spec/concerns/daemon.md +++ b/spec/concerns/daemon.md @@ -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 기반. @@ -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 diff --git a/spec/concerns/lifecycle-hook.md b/spec/concerns/lifecycle-hook.md index c227294..346aba3 100644 --- a/spec/concerns/lifecycle-hook.md +++ b/spec/concerns/lifecycle-hook.md @@ -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 실행 루프 변경 @@ -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` 비교) 캐시를 무효화한다. --- @@ -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 반응)이 명확히 분리된다