745 lines
23 KiB
Markdown
745 lines
23 KiB
Markdown
# EJClaw 레거시 호환 제거 및 구조 정리 Spec
|
|
|
|
- 상태: Draft
|
|
- 대상: 코어 개발자 / 리팩토링 담당자
|
|
- 기준일: 2026-04-09
|
|
- 기준: 저장소 정적 분석 결과
|
|
- 참고: 현재 환경에는 Bun 런타임이 없어 테스트/실행 검증은 하지 못했고, 이 문서는 코드 구조와 저장소 상태를 기준으로 작성했다.
|
|
|
|
## 0. 한 줄 결론
|
|
|
|
이 저장소의 핵심 문제는 **문서상 SSOT는 하나인데, 실제 런타임/설정/검증 경로에는 예전 방식이 계속 살아 있어서 같은 의미가 2~3군데에서 동시에 정의되는 점**이다. 이 브랜치의 목표는 “조금 더 예쁘게 정리”가 아니라, **레거시 호환 경로를 런타임에서 완전히 제거하고, 데이터 모델과 실행 경로를 단일화하는 것**이다.
|
|
|
|
---
|
|
|
|
## 1. 문제 정의
|
|
|
|
현재 저장소는 README 상으로는 다음을 선언하고 있다.
|
|
|
|
- `room_settings`가 room-level SSOT
|
|
- 공개 room assignment 인터페이스는 `assign_room`
|
|
- `registered_groups`는 legacy fallback 성격의 materialized layer
|
|
|
|
하지만 실제 코드는 아래와 같이 동작한다.
|
|
|
|
1. setup 경로는 여전히 `registered_groups`에 직접 쓴다.
|
|
2. 앱 시작 시 `registered_groups`를 읽어서 `room_settings`를 다시 복원한다.
|
|
3. verify/setup/환경 점검은 `room_settings`와 `registered_groups`를 동시에 본다.
|
|
4. 일부 런타임 읽기 로직은 저장된 값이 부족하면 `room_settings`, `registered_groups`, `folder`, `service shadow`를 조합해서 의미를 “추론”한다.
|
|
5. 레거시 JSON 파일과 환경변수 alias도 아직 읽는다.
|
|
|
|
즉, 현재 버그의 근본 원인은 단순한 코드 스타일 문제가 아니라, **도메인 의미가 여러 저장소/레이어에 중복 저장되고, 부족한 값을 읽기 시점에 복원하는 구조**에 있다.
|
|
|
|
---
|
|
|
|
## 2. 이 리팩토링의 목표
|
|
|
|
### 목표
|
|
|
|
1. **단일 진실원(SSOT) 확립**
|
|
- room 메타데이터와 실행 설정은 하나의 canonical schema만 사용한다.
|
|
|
|
2. **런타임에서 레거시 호환 제거**
|
|
- startup, read-path, verify-path 어디에서도 legacy row/json/env alias를 fallback으로 읽지 않는다.
|
|
|
|
3. **쓰기 시 canonical, 읽기 시 deterministic**
|
|
- 새 데이터는 항상 완전한 canonical 형태로 저장한다.
|
|
- 읽을 때 추론/복원/재구성을 하지 않는다.
|
|
|
|
4. **책임 분리(SRP)**
|
|
- setup, migration, runtime, delivery, scheduler, agent process spawning의 책임을 분리한다.
|
|
|
|
5. **작은 단위로 안전하게 삭제**
|
|
- “리팩토링하면서 조금씩 살리기”가 아니라, migration → cutover → 삭제 순으로 간다.
|
|
|
|
### 비목표
|
|
|
|
1. Tribunal 동작 규칙 자체를 새로 설계하지 않는다.
|
|
2. Discord 채널 UX를 바꾸는 작업은 이번 범위의 중심이 아니다.
|
|
3. 모델/프로바이더 전략 자체를 갈아엎는 작업은 아니다.
|
|
4. 대규모 네이밍 변경을 첫 PR에서 같이 하지 않는다.
|
|
- 동작 변경과 rename을 한 PR에 섞지 않는다.
|
|
|
|
---
|
|
|
|
## 3. 저장소 감사 결과
|
|
|
|
### 3.1 SSOT 선언과 실제 구현이 다르다
|
|
|
|
문서에서는 `room_settings`를 SSOT라고 적고 있지만, 실제 동작에서는 `registered_groups`가 아직도 쓰기/읽기/복원 경로에 살아 있다.
|
|
|
|
| 위치 | 관찰 내용 | 의미 |
|
|
| ------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
|
|
| `README.md:37-43` | `room_settings`가 SSOT라고 문서화 | 설계 의도는 명확함 |
|
|
| `setup/register.ts:116-160` | setup 단계가 `registered_groups`를 직접 생성/쓰기 | 공식 setup 경로가 canonical write path를 우회 |
|
|
| `src/db.ts:810-878` | `assignRoom()`이 `room_settings`를 쓰면서 동시에 `registered_groups`를 materialize | 하나의 행위를 두 저장소에 반영 |
|
|
| `src/db.ts:1000-1016` | startup 시 `registered_groups`를 읽어서 `room_settings`를 다시 sync | 런타임이 legacy projection을 source처럼 사용 |
|
|
| `src/db.ts:907-935` | `getAllRegisteredGroups()`가 stored rows + legacy rows를 합쳐서 반환 | read path가 dual-source |
|
|
| `setup/verify-state.ts:124-158` | tribunal room 계산 시 `room_settings`와 `registered_groups`를 UNION/fallback | verify도 dual-source |
|
|
|
|
### 3.2 setup 경로가 SRP를 심하게 위반한다
|
|
|
|
`setup/register.ts`는 실제로 아래를 한 번에 수행한다.
|
|
|
|
1. SQLite 테이블 생성/쓰기
|
|
2. 그룹 폴더 생성
|
|
3. `groups/.../CLAUDE.md` 수정
|
|
4. `.env`의 `ASSISTANT_NAME` 수정
|
|
|
|
특히 `setup/register.ts:170-214`는 채널 등록 단계가 assistant identity까지 바꾸고 있다. 이건 room assignment와 완전히 다른 책임이다.
|
|
|
|
결론: **setup/register는 room 등록 도구가 아니라, 여러 전역 설정을 몰래 바꾸는 side-effect command**다.
|
|
|
|
### 3.3 startup이 암묵적 마이그레이션을 수행한다
|
|
|
|
`src/db/bootstrap.ts:46-92`는 startup 흐름에서 아래 파일을 읽고 `.migrated`로 rename한다.
|
|
|
|
- `router_state.json`
|
|
- `sessions.json`
|
|
- `registered_groups.json`
|
|
|
|
또 `setup/environment.ts:45-58`는 아직도 `registered_groups.json` 존재 여부를 setup 상태 판단에 사용한다.
|
|
|
|
결론: **앱 시작 자체가 데이터 정리/마이그레이션 도구 역할을 하고 있어 재현 가능성과 디버깅 가능성을 해친다.**
|
|
|
|
### 3.4 읽기 시점 복원 로직이 많다
|
|
|
|
`src/db/legacy-rebuilds.ts:19-96`와 `src/db/paired-state.ts:85-120`는 저장된 값이 부족해도 아래 소스를 뒤져서 owner/reviewer agent type을 복원한다.
|
|
|
|
- persisted row
|
|
- `room_settings`
|
|
- `registered_groups` by jid
|
|
- `registered_groups` by folder
|
|
- service shadow
|
|
|
|
이 구조에서는 row가 불완전해도 시스템이 계속 돌아가므로, 데이터가 어긋난 상태가 초기에 드러나지 않고 런타임 버그로 누적된다.
|
|
|
|
결론: **canonical write가 아니라 read-time repair에 의존하고 있다.**
|
|
|
|
### 3.5 legacy alias가 계속 살아 있다
|
|
|
|
현재 프로덕션 코드가 여전히 읽는 레거시 alias/호환 키:
|
|
|
|
- Discord bot token alias (`src/channels/discord.ts:30-63`)
|
|
- `DISCORD_BOT_TOKEN`
|
|
- `DISCORD_CLAUDE_BOT_TOKEN`
|
|
- `DISCORD_CODEX_BOT_TOKEN`
|
|
- `DISCORD_CODEX_MAIN_BOT_TOKEN`
|
|
- `DISCORD_REVIEW_BOT_TOKEN`
|
|
- `DISCORD_CODEX_REVIEW_BOT_TOKEN`
|
|
- session command sender alias (`src/config/load-config.ts:218-223`)
|
|
- `SESSION_COMMAND_USER_IDS`
|
|
- router state legacy key alias (`src/index.ts:172-203`)
|
|
- `last_timestamp` → `last_seq`
|
|
- `last_agent_timestamp` → `last_agent_seq`
|
|
|
|
결론: **canonical config/state 이름이 있어도 시스템은 구 이름을 받아주기 때문에, 실제 운영 상태를 확신하기 어렵다.**
|
|
|
|
### 3.6 God file / high coupling이 심하다
|
|
|
|
정적 집계 기준, 주요 production 파일 크기는 다음과 같다.
|
|
|
|
- `src/db.ts` — 1342 LOC
|
|
- `src/db/schema.ts` — 1139 LOC
|
|
- `src/group-queue.ts` — 1044 LOC
|
|
- `src/message-agent-executor.ts` — 910 LOC
|
|
- `src/channels/discord.ts` — 892 LOC
|
|
- `src/ipc.ts` — 858 LOC
|
|
- `src/paired-workspace-manager.ts` — 850 LOC
|
|
- `src/message-runtime.ts` — 757 LOC
|
|
- `src/index.ts` — 569 LOC
|
|
- `src/agent-runner.ts` — 557 LOC
|
|
|
|
추가로 함수 길이도 길다.
|
|
|
|
- `createMessageRuntime()` — 636 LOC
|
|
- `runAgentProcess()` — 494 LOC
|
|
- `runTask()` — 367 LOC
|
|
- `main()` — 237 LOC
|
|
|
|
또한 `db.js` façade를 직접 import하는 파일이 47개다.
|
|
|
|
결론: **현재 구조는 개별 버그 수정이 곧 전체 시스템 리스크로 이어지는 결합 형태**다.
|
|
|
|
### 3.7 runner 계층 중복이 있다
|
|
|
|
- `runners/agent-runner/src/room-role-context.ts` — 23 LOC
|
|
- `runners/codex-runner/src/room-role-context.ts` — 23 LOC (동일)
|
|
|
|
`reviewer-runtime.ts`도 역할 정책이라는 같은 도메인을 두 러너가 따로 유지한다.
|
|
|
|
결론: **러너별 구현 차이가 아니라 공통 정책 코드의 중복 유지비가 발생하고 있다.**
|
|
|
|
### 3.8 테스트도 레거시 호환에 많이 묶여 있다
|
|
|
|
정적 검색상 `legacy|backfill|compat|backward` 키워드가 테스트 코드에서 247개 라인에 등장한다.
|
|
|
|
문제는 테스트가 많다는 점이 아니라, **원하는 최종 행동보다 과거 호환 동작을 보호하는 표면적이 너무 넓다**는 점이다.
|
|
|
|
---
|
|
|
|
## 4. 핵심 설계 결정
|
|
|
|
### 결정 1. `registered_groups`는 더 이상 runtime source가 아니다
|
|
|
|
최종 목표는 아래 둘 중 하나다.
|
|
|
|
1. **권장안:** `registered_groups` 테이블 자체 제거
|
|
2. **차선안:** SQL VIEW 또는 in-memory projection으로만 유지
|
|
- 단, write path 금지
|
|
- source로 사용 금지
|
|
|
|
이번 브랜치의 목표가 “레거시 호환 제거”인 만큼, 최종 상태는 **삭제**를 기본으로 한다.
|
|
|
|
### 결정 2. canonical persistence를 아래처럼 재구성한다
|
|
|
|
#### 4.2.1 room-level truth
|
|
|
|
`room_settings`를 room-level canonical table로 유지하되, 필요한 필드를 보강한다.
|
|
|
|
권장 canonical field:
|
|
|
|
- `chat_jid`
|
|
- `name`
|
|
- `folder`
|
|
- `trigger_pattern`
|
|
- `requires_trigger`
|
|
- `is_main`
|
|
- `work_dir`
|
|
- `room_mode`
|
|
- `owner_agent_type`
|
|
- `created_at`
|
|
- `updated_at`
|
|
|
|
#### 4.2.2 role/runtime override 분리
|
|
|
|
현재 `registered_groups.agent_config`가 사실상 role/runtime override 역할까지 떠안고 있으므로, 아래 신규 테이블을 추가한다.
|
|
|
|
`room_role_overrides`
|
|
|
|
- `chat_jid`
|
|
- `role` (`owner` | `reviewer` | `arbiter`)
|
|
- `agent_type`
|
|
- `agent_config_json`
|
|
- `created_at`
|
|
- `updated_at`
|
|
- PK: (`chat_jid`, `role`)
|
|
|
|
이렇게 하면
|
|
|
|
- room-level 설정은 `room_settings`
|
|
- role/runtime override는 `room_role_overrides`
|
|
- 실제 실행용 DTO는 코드에서 조립
|
|
|
|
으로 명확히 나뉜다.
|
|
|
|
### 결정 3. startup은 마이그레이션을 수행하지 않는다
|
|
|
|
startup이 해도 되는 일:
|
|
|
|
- DB open
|
|
- schema version check
|
|
- required table existence check
|
|
- fail fast
|
|
|
|
startup이 하면 안 되는 일:
|
|
|
|
- JSON state 읽고 rename
|
|
- legacy table 보고 canonical row 재구성
|
|
- 불완전 row를 추론으로 보정
|
|
|
|
즉, `src/db/bootstrap.ts`의 JSON migration과 `src/db.ts`의 `syncLegacyRegisteredGroupsIntoStoredRooms()`는 **명시적 migration command**로 옮기고 런타임 경로에서 제거한다.
|
|
|
|
### 결정 4. 읽기 시 복원 대신 쓰기 시 canonical complete row를 강제한다
|
|
|
|
새로 생성되는 row는 항상 아래를 완전하게 가져야 한다.
|
|
|
|
- paired task: owner/reviewer/arbiter agent type
|
|
- paired task: owner/reviewer service id
|
|
- channel owner lease: role별 agent/service metadata
|
|
- service handoff/work item: target/source role 및 canonical service id
|
|
|
|
읽기 시에는 아래를 금지한다.
|
|
|
|
- folder 기반 agent type 추론
|
|
- legacy table 기반 fallback
|
|
- service id shadow를 보고 agent type 재추론
|
|
|
|
`legacy-rebuilds.ts`는 migration 기간이 끝나면 삭제한다.
|
|
|
|
### 결정 5. setup은 application service를 호출해야 한다
|
|
|
|
`setup/register.ts`처럼 setup 코드가 직접 SQL을 치고, 파일 시스템을 수정하고, `.env`를 바꾸는 구조를 금지한다.
|
|
|
|
setup은 아래 중 하나만 해야 한다.
|
|
|
|
- `assignRoom()` 같은 application service 호출
|
|
- dedicated config command 호출
|
|
|
|
즉,
|
|
|
|
- room assignment
|
|
- assistant name 변경
|
|
- prompt/CLAUDE.md 템플릿 관리
|
|
|
|
는 서로 다른 command로 분리한다.
|
|
|
|
### 결정 6. 네이밍은 “동작 정리 후” 바꾼다
|
|
|
|
현재 `RegisteredGroup`은 이미 room binding/runtime binding DTO에 더 가깝다. 하지만 첫 단계에서 타입 rename까지 섞으면 diff가 너무 커진다.
|
|
|
|
권장 순서:
|
|
|
|
1. 동작 cutover
|
|
2. legacy source 삭제
|
|
3. 그 다음 `RegisteredGroup` → `RoomBinding` rename
|
|
|
|
---
|
|
|
|
## 5. 목표 아키텍처
|
|
|
|
### 5.1 도메인 모델
|
|
|
|
#### Room
|
|
|
|
room의 room-level truth.
|
|
|
|
- chatJid
|
|
- name
|
|
- folder
|
|
- triggerPattern
|
|
- requiresTrigger
|
|
- isMain
|
|
- workDir
|
|
- roomMode
|
|
- ownerAgentType
|
|
- createdAt
|
|
- updatedAt
|
|
|
|
#### RoomRoleOverride
|
|
|
|
room별 role/runtime override.
|
|
|
|
- chatJid
|
|
- role
|
|
- agentType
|
|
- agentConfig
|
|
- createdAt
|
|
- updatedAt
|
|
|
|
#### RoomBinding
|
|
|
|
실행 시점 DTO. DB에 저장하지 않는다.
|
|
|
|
- room
|
|
- role
|
|
- effectiveAgentType
|
|
- effectiveServiceId
|
|
- effectiveChannelName
|
|
- effectiveAgentConfig
|
|
- workDir
|
|
|
|
### 5.2 레이어 구조
|
|
|
|
권장 구조:
|
|
|
|
```text
|
|
src/
|
|
app/
|
|
bootstrap.ts
|
|
shutdown.ts
|
|
domain/
|
|
rooms/
|
|
room-service.ts
|
|
room-binding.ts
|
|
room-migration.ts
|
|
paired/
|
|
paired-task-service.ts
|
|
handoff-service.ts
|
|
delivery/
|
|
work-item-service.ts
|
|
repositories/
|
|
rooms-repository.ts
|
|
room-role-overrides-repository.ts
|
|
paired-tasks-repository.ts
|
|
messages-repository.ts
|
|
router-state-repository.ts
|
|
runtime/
|
|
message-loop/
|
|
ingress.ts
|
|
turn-coordinator.ts
|
|
delivery-coordinator.ts
|
|
recovery.ts
|
|
agent-process/
|
|
process-runner.ts
|
|
output-parser.ts
|
|
timeout-controller.ts
|
|
run-logger.ts
|
|
runners/
|
|
shared/
|
|
room-role-context.ts
|
|
reviewer-policy.ts
|
|
```
|
|
|
|
핵심은 **composition root**, **domain service**, **repository**, **runtime coordinator**를 분리하는 것이다.
|
|
|
|
---
|
|
|
|
## 6. 단계별 실행 계획
|
|
|
|
## Phase 0. 리팩토링 가드레일 추가
|
|
|
|
### 해야 할 일
|
|
|
|
1. `db.ts` 신규 export 금지
|
|
2. 신규 코드에서 legacy key/table 사용 금지
|
|
3. startup 경로에 migration 로직 추가 금지
|
|
4. room assignment는 오직 하나의 application service만 통해서 수행
|
|
|
|
### 산출물
|
|
|
|
- ADR 또는 짧은 architecture note
|
|
- lint/grep 기반 CI guard 추가
|
|
|
|
### 통과 기준
|
|
|
|
- 새 PR이 `registered_groups`/legacy env alias를 새로 참조하지 않음
|
|
|
|
---
|
|
|
|
## Phase 1. schema cutover 준비
|
|
|
|
### 해야 할 일
|
|
|
|
1. `room_settings`에 canonical field 부족분 추가
|
|
- 최소: `created_at`
|
|
2. `room_role_overrides` 신규 테이블 추가
|
|
3. explicit migration command 구현
|
|
- 기존 `scripts/run-migrations.ts` 체계를 활용 가능
|
|
4. migration report 생성
|
|
- migrated rows
|
|
- skipped rows
|
|
- conflict rows
|
|
|
|
### migration 규칙
|
|
|
|
1. `registered_groups`를 room 단위로 fold
|
|
2. room-level 필드 충돌 시 fail fast
|
|
- name/folder/requires_trigger/is_main/work_dir conflict는 자동 복구 금지
|
|
3. owner/reviewer runtime row는 `room_role_overrides`로 이동
|
|
4. `registered_groups.json`도 같은 규칙으로 fold
|
|
5. 성공 시 legacy backup table/file로 보관
|
|
- 예: `registered_groups_legacy_backup`
|
|
- 예: `registered_groups.json.migrated`
|
|
|
|
### 통과 기준
|
|
|
|
- migration 명령이 idempotent
|
|
- conflict가 있으면 startup이 아니라 migration 단계에서 실패
|
|
|
|
---
|
|
|
|
## Phase 2. write path 단일화
|
|
|
|
### 해야 할 일
|
|
|
|
1. `assignRoom()`이 canonical table만 쓰도록 변경
|
|
2. `materializeRegisteredGroupsForRoom()` 제거
|
|
3. `setup/register.ts` 제거 또는 `setup/assign-room.ts`로 대체
|
|
4. assistant name 변경 로직을 별도 step으로 분리
|
|
5. setup/verify/environment 경로가 canonical table만 보게 수정
|
|
|
|
### 명시적 삭제 대상
|
|
|
|
- `setup/register.ts`의 direct SQL write
|
|
- `setup/register.ts`의 `.env` 수정
|
|
- `setup/register.ts`의 `CLAUDE.md` 수정
|
|
- verify output의 legacy field
|
|
|
|
### 통과 기준
|
|
|
|
- room 생성/수정 write path가 하나뿐임
|
|
- setup/verify도 그 write path를 공유함
|
|
|
|
---
|
|
|
|
## Phase 3. read path 단일화
|
|
|
|
### 해야 할 일
|
|
|
|
1. `getAllRegisteredGroups()` 제거 또는 `getAllRoomBindings()`로 대체
|
|
2. runtime에서 `room_settings` + `room_role_overrides`만 읽도록 수정
|
|
3. `syncLegacyRegisteredGroupsIntoStoredRooms()` 삭제
|
|
4. `getLegacyRegisteredGroupRows()` / `getLegacyRegisteredGroup()` 삭제
|
|
5. `buildRegisteredGroupFromStoredSettings()`는 남기더라도 source는 canonical table만 사용
|
|
|
|
### 권장 API
|
|
|
|
- `getRoom(chatJid)`
|
|
- `listRooms()`
|
|
- `buildRoomBinding(chatJid, role)`
|
|
- `listRoomBindings()`
|
|
|
|
### 통과 기준
|
|
|
|
- production code에서 `registered_groups` 참조 0
|
|
- production code에서 `registered_groups.json` 참조 0
|
|
|
|
---
|
|
|
|
## Phase 4. read-time repair 제거
|
|
|
|
### 해야 할 일
|
|
|
|
1. paired task / lease / handoff / work item row migration 수행
|
|
2. row creation 시 canonical service/agent metadata 강제
|
|
3. `legacy-rebuilds.ts` 삭제
|
|
4. `paired-state.ts` hydrate 시 fallback 추론 제거
|
|
5. read path는 incomplete row를 만나면 fail fast + operator action 요구
|
|
|
|
### 통과 기준
|
|
|
|
- `legacy-rebuilds.ts` 삭제
|
|
- `resolveStablePairedTaskOwnerAgentType()` 류의 fallback 제거
|
|
- canonical fields 없는 row가 조용히 복원되지 않음
|
|
|
|
---
|
|
|
|
## Phase 5. 구조 분해
|
|
|
|
### 해야 할 일
|
|
|
|
1. `src/index.ts`를 composition root로 축소
|
|
2. `src/message-runtime.ts` 분해
|
|
- ingress
|
|
- pending turn coordinator
|
|
- delivery coordinator
|
|
- recovery coordinator
|
|
3. `src/agent-runner.ts` 분해
|
|
- env builder
|
|
- process runner
|
|
- output parser
|
|
- timeout policy
|
|
- run log persistence
|
|
4. `src/db.ts` façade 축소 또는 제거
|
|
5. `runners/shared` 추출
|
|
|
|
### 우선 분해 대상
|
|
|
|
1. `src/db.ts`
|
|
2. `src/message-runtime.ts`
|
|
3. `src/index.ts`
|
|
4. `src/agent-runner.ts`
|
|
5. `src/ipc.ts`
|
|
6. `src/group-queue.ts`
|
|
|
|
### 권장 가드레일
|
|
|
|
- 일반 production file 400 LOC 초과 금지
|
|
- 일반 function 80~100 LOC 초과 금지
|
|
- setup/command는 single responsibility 유지
|
|
|
|
> 위 숫자는 절대 규칙이라기보다, 지금처럼 God file로 재응집되는 것을 막기 위한 guardrail이다.
|
|
|
|
---
|
|
|
|
## Phase 6. config/state legacy alias 제거
|
|
|
|
### 삭제 대상
|
|
|
|
#### 환경변수 alias
|
|
|
|
- `DISCORD_BOT_TOKEN`
|
|
- `DISCORD_CLAUDE_BOT_TOKEN`
|
|
- `DISCORD_CODEX_BOT_TOKEN`
|
|
- `DISCORD_CODEX_MAIN_BOT_TOKEN`
|
|
- `DISCORD_REVIEW_BOT_TOKEN`
|
|
- `DISCORD_CODEX_REVIEW_BOT_TOKEN`
|
|
- `SESSION_COMMAND_USER_IDS`
|
|
|
|
#### router state alias
|
|
|
|
- `last_timestamp`
|
|
- `last_agent_timestamp`
|
|
|
|
#### JSON migration source
|
|
|
|
- `router_state.json`
|
|
- `sessions.json`
|
|
- `registered_groups.json`
|
|
|
|
### 정책
|
|
|
|
- alias를 더는 읽지 않는다.
|
|
- deprecated key가 발견되면 warning이 아니라 startup error를 내고 canonical key명을 안내한다.
|
|
|
|
### 통과 기준
|
|
|
|
- production code에서 legacy alias string이 남아 있지 않음
|
|
|
|
---
|
|
|
|
## 7. 추천 PR 분할
|
|
|
|
### PR 1. Schema + explicit migration command
|
|
|
|
- `room_role_overrides` 추가
|
|
- migration report 작성
|
|
- legacy backup 전략 도입
|
|
|
|
### PR 2. Room assignment write-path cutover
|
|
|
|
- `assignRoom()` canonical-only
|
|
- setup/register 제거/대체
|
|
- verify/environment canonical-only
|
|
|
|
### PR 3. Runtime read-path cutover
|
|
|
|
- `registered_groups` 참조 제거
|
|
- `getAllRoomBindings()` 도입
|
|
- startup sync 제거
|
|
|
|
### PR 4. Paired task canonicalization
|
|
|
|
- paired/channel owner/handoff/work item backfill
|
|
- `legacy-rebuilds.ts` 삭제
|
|
|
|
### PR 5. Config/state alias 제거
|
|
|
|
- env alias 삭제
|
|
- router state alias 삭제
|
|
- startup JSON migration 삭제
|
|
|
|
### PR 6. 구조 분해
|
|
|
|
- `index.ts`, `message-runtime.ts`, `agent-runner.ts`, `db.ts` 분해
|
|
- `runners/shared` 추출
|
|
|
|
---
|
|
|
|
## 8. 테스트 전략 변경
|
|
|
|
### 기존 문제
|
|
|
|
현재 테스트는 legacy/backfill 호환 보호 비중이 크다.
|
|
|
|
### 바꿔야 할 방향
|
|
|
|
1. **runtime 테스트**는 canonical behavior만 검증
|
|
2. **migration 테스트**는 explicit migration command에만 집중
|
|
3. legacy 호환 테스트는 runtime suite에서 제거
|
|
4. startup은 migration을 하지 않는다는 계약을 테스트로 고정
|
|
|
|
### 반드시 추가할 테스트
|
|
|
|
1. canonical room assignment round-trip
|
|
2. migration conflict detection
|
|
3. incomplete canonical row fail-fast
|
|
4. setup/assign-room side-effect isolation
|
|
5. startup idempotence
|
|
6. paired task hydrate가 fallback 없이 동작함을 보장하는 테스트
|
|
|
|
---
|
|
|
|
## 9. 완료 기준(Definition of Done)
|
|
|
|
아래를 모두 만족해야 “레거시 제거 완료”로 본다.
|
|
|
|
### 데이터/설정
|
|
|
|
- `registered_groups`가 runtime source가 아님
|
|
- startup이 JSON/legacy row를 읽어 canonical row를 만들지 않음
|
|
- paired/task/handoff/lease row가 canonical metadata를 완전하게 저장함
|
|
|
|
### 코드
|
|
|
|
- `src/db/legacy-rebuilds.ts` 삭제
|
|
- `setup/register.ts` 삭제 또는 room assignment 전용 thin wrapper로 교체
|
|
- production code에서 legacy env alias 참조 0
|
|
- production code에서 legacy state key 참조 0
|
|
- production code에서 `registered_groups` 참조 0
|
|
|
|
### 구조
|
|
|
|
- `src/index.ts`는 bootstrap/composition root 역할만 수행
|
|
- `src/message-runtime.ts`는 coordinator 분해 완료
|
|
- `src/agent-runner.ts`는 process runner 전용 책임으로 축소
|
|
- `db.js` façade 직접 import를 강하게 축소하거나 제거
|
|
|
|
### 운영
|
|
|
|
- migration은 explicit command로만 수행
|
|
- deprecated key가 있으면 startup error로 조기 발견
|
|
- 문서와 코드가 같은 canonical path를 설명함
|
|
|
|
---
|
|
|
|
## 10. 바로 먼저 고쳐야 할 것들
|
|
|
|
전체 리팩토링 전에 가장 빨리 효과가 나는 순서:
|
|
|
|
1. `setup/register.ts`에서 `.env` / `CLAUDE.md` 수정 제거
|
|
2. startup의 `syncLegacyRegisteredGroupsIntoStoredRooms()` 제거 준비
|
|
3. explicit migration command 먼저 도입
|
|
4. verify/environment에서 `registered_groups.json`/`registered_groups` fallback 제거
|
|
5. `buildVerifySummary()`의 의미 어긋난 네이밍 정리
|
|
- 현재 `codexConfigured`가 사실상 reviewer bot configured 의미
|
|
- `reviewConfigured`가 arbiter bot configured 의미
|
|
|
|
이 5개만 먼저 해도 “버그가 곳곳에서 새는 느낌”이 크게 줄어든다.
|
|
|
|
---
|
|
|
|
## 11. 요약
|
|
|
|
이 저장소는 단순히 코드가 긴 게 문제가 아니다.
|
|
|
|
핵심 문제는 다음 세 가지다.
|
|
|
|
1. **같은 의미를 여러 저장소가 동시에 들고 있음**
|
|
2. **런타임이 과거 데이터를 읽으면서 현재 의미를 추론함**
|
|
3. **setup/runtime/migration 책임이 섞여 있음**
|
|
|
|
따라서 정리 순서는 반드시 아래여야 한다.
|
|
|
|
1. schema와 canonical model을 먼저 고정
|
|
2. explicit migration 제공
|
|
3. write path cutover
|
|
4. read path cutover
|
|
5. legacy 삭제
|
|
6. 그 다음에 큰 파일 분해와 rename
|
|
|
|
즉, 이번 작업의 핵심은 “클린코드 스타일 적용”이 아니라,
|
|
|
|
> **호환 경로 삭제 → 진실원 단일화 → 읽기 시 추론 제거 → 책임 분리**
|
|
|
|
이다.
|
|
|
|
---
|
|
|
|
## 12. 추천 자동 검증 명령
|
|
|
|
아래 검색은 cleanup branch에서 CI guard로 넣는 것을 권장한다.
|
|
|
|
```bash
|
|
# legacy table / json source
|
|
rg -n "registered_groups|registered_groups.json" src setup runners
|
|
|
|
# legacy migration-on-startup
|
|
rg -n "migrateJsonStateFromFiles|syncLegacyRegisteredGroupsIntoStoredRooms" src setup runners
|
|
|
|
# legacy env aliases
|
|
rg -n "DISCORD_BOT_TOKEN|DISCORD_CLAUDE_BOT_TOKEN|DISCORD_CODEX_BOT_TOKEN|DISCORD_CODEX_MAIN_BOT_TOKEN|DISCORD_REVIEW_BOT_TOKEN|DISCORD_CODEX_REVIEW_BOT_TOKEN|SESSION_COMMAND_USER_IDS" src setup runners
|
|
|
|
# legacy router state aliases
|
|
rg -n "last_timestamp|last_agent_timestamp" src setup runners
|
|
|
|
# read-time repair helpers
|
|
rg -n "legacy-rebuilds|resolveStablePairedTaskOwnerAgentType|resolveStableReviewerAgentType" src setup runners
|
|
```
|
|
|
|
최종 상태에서는 위 검색이 아래 중 하나여야 한다.
|
|
|
|
- 0건
|
|
- migration command / legacy backup doc / changelog 같은 명시적 예외 파일에만 존재
|