Linear 이슈를 자동으로 폴링해서 이슈별 격리 워크스페이스를 생성하고, 코딩 에이전트(Codex 또는 Claude Code)를 실행해주는 오케스트레이터 데몬.
- macOS
- Codex CLI 또는 Claude Code
curl -fsSL https://raw.githubusercontent.com/J132134/symphony/main/scripts/install.sh | bash~/.local/bin/symphony에 최신 릴리스 바이너리를 설치한다.
LaunchAgent(데몬 자동 시작)까지 한 번에 설치:
curl -fsSL https://raw.githubusercontent.com/J132134/symphony/main/scripts/install.sh | bash -s -- --with-launchagentsLINEAR_API_KEY 환경변수가 설정되어 있으면 plist에 자동으로 주입된다.
curl -fsSL https://raw.githubusercontent.com/J132134/symphony/main/scripts/install.sh | bashlaunchctl list | grep symphony
# com.symphony.daemon → 데몬git clone https://github.com/J132134/symphony.git
cd symphony
# 바이너리 빌드 → ~/.local/bin/symphony
make install
# LaunchAgent 등록
make install-launchagentsmake install-launchagents는 scripts/ 안의 plist 템플릿에서 현재 홈 디렉토리와 로그 디렉토리를 채워 ~/Library/LaunchAgents/에 설치한다. 로그는 ~/Library/Logs/Symphony에 기록되며, launchd 작업 디렉터리도 레포로 고정하지 않으므로 레포가 Documents나 Desktop 아래 있어도 데몬 시작만으로 해당 보호 폴더 접근 알림을 불필요하게 띄우지 않는다.
프로젝트별 workflow는 ~/.config/symphony/workflows/ 아래에 두는 구성을 권장한다. YAML front matter가 설정이고, --- 이후가 에이전트에 전달되는 Jinja2 호환 프롬프트 템플릿이다. 공통 base를 쓸 때는 overlay 파일 상단에 workflow_base를 둔다.
---
workflow_base: ~/.config/symphony/workflows/linear-team-base.md
tracker:
project_slug: my-project
hooks:
after_create: |
git clone --depth 1 https://github.com/example/my-project.git .
make setup
project:
name: my-project
summary: 내 프로젝트 설명.
conventions:
- "빌드: make build"
- "테스트: make test"
validation_commands:
- make test
---
## 프로젝트 추가 지침
- 빌드 전 `make generate`가 필요하면 항상 먼저 실행한다.~/.config/symphony/config.yaml에 공통 base와 프로젝트 overlay를 등록한다.
projects:
- name: my-project
workflow_base: ~/.config/symphony/workflows/linear-team-base.md
workflow: ~/.config/symphony/workflows/my-project.mdexport LINEAR_API_KEY=lin_api_xxxxxxxxxxxxxxxx또는 .env 파일:
LINEAR_API_KEY=lin_api_xxxxxxxxxxxxxxxx
# 설정 검증
symphony validate --workflow ~/.config/symphony/workflows/my-project.md
# 실행
symphony run --workflow ~/.config/symphony/workflows/my-project.md
# 대시보드 포함
symphony run --workflow ~/.config/symphony/workflows/my-project.md --port 8080symphony validate는 기본적으로 ~/.config/symphony/config.yaml을 함께 읽어, 해당 workflow가 등록된 프로젝트라면 그 프로젝트 entry를 기준으로 workflow를 찾고, 공통 base는 overlay 파일 상단의 workflow_base에서 읽어 merged workflow를 검증한다. 다른 config 파일을 쓰는 경우 --config /path/to/config.yaml을 추가한다. 프로젝트별 workflow overlay를 ~/.config/symphony/workflows/에 두고 config에서 참조하는 구성을 권장한다.
여러 프로젝트를 단일 프로세스로 실행하려면 ~/.config/symphony/config.yaml을 작성한다.
projects:
- name: backend
workflow: ~/.config/symphony/workflows/backend.md
- name: frontend
workflow: ~/.config/symphony/workflows/frontend.md
agent:
max_total_concurrent_sessions: 4
status_server:
enabled: true
port: 7777
webhook:
enabled: false
port: 7778
bind_address: 127.0.0.1
signing_secret: $LINEAR_WEBHOOK_SECRET
auto_update:
enabled: true
interval_minutes: 30symphony daemon
# 또는 커스텀 설정 경로
symphony daemon --config /path/to/config.yamlsymphony daemon은 실행 중에도 config.yaml의 변경을 감지해 설정을 다시 읽는다. 새 설정이 유효하면 프로젝트 목록 diff를 계산해 바뀐 프로젝트만 선택적으로 시작, 교체, 종료하고, 변경 없는 프로젝트는 그대로 유지한다. status_server, webhook, auto_update, agent.max_total_concurrent_sessions 같은 daemon 전역 설정이 바뀐 경우에만 상태 서버, webhook 서버, update loop를 포함한 전체 runtime을 다시 띄운다. 유효하지 않은 설정은 적용하지 않고 기존 실행 상태를 유지한 채 오류만 로그에 남긴다.
config.yaml 안의 상대 경로(projects[].workflow)는 현재 셸의 작업 디렉터리가 아니라 config.yaml 파일이 있는 디렉터리 기준으로 해석된다. launch agent가 특정 레포 디렉터리를 작업 디렉터리로 잡지 않아도 동일하게 동작하도록 하기 위한 동작이다. overlay 파일 안의 workflow_base는 해당 overlay 파일 위치 기준으로 해석된다.
agent.max_total_concurrent_sessions는 데몬 전체에서 동시에 실행할 수 있는 에이전트 세션 수 상한이다. 값을 생략하면 실행 중인 머신의 CPU 개수를 기준으로 동적으로 계산한다: NumCPU() <= 2면 1, <= 4면 2, 그 외에는 NumCPU()/2를 사용하되 최대 8로 제한한다. 각 프로젝트의 WORKFLOW.md에 있는 agent.max_concurrent_agents와 agent.max_concurrent_agents_by_state는 그대로 유지되며, 실제 dispatch는 프로젝트별 전체 제한, 상태별 제한, 데몬 전체 제한을 모두 만족해야 한다.
각 프로젝트의 WORKFLOW.md에는 daemon.drain_timeout_ms를 둘 수 있다. graceful drain의 기본값은 codex.stall_timeout_ms + hooks.timeout_ms이며, hot-reload나 shutdown 중에는 새 작업을 막은 뒤 현재 turn/tool call과 after_run, before_remove 훅이 이 상한 안에서 끝나기를 기다린다. 상한을 넘기면 Codex subprocess는 SIGTERM, 10초 후 SIGKILL 순서로 종료된다. full runtime reload는 new runtime start -> old runtime drain 순서로 실행돼 세션 공백을 줄인다.
Linear webhook은 데몬 모드에서만 사용된다. webhook이 켜지면 각 프로젝트 orchestrator는 짧은 active/idle 폴링 대신 polling.webhook_fallback_interval_ms 간격의 장주기 폴백 폴링만 유지하고, 실제 즉시 refresh는 /webhook/linear 요청으로 트리거된다.
webhook:
enabled: true
port: 7778
bind_address: 127.0.0.1
signing_secret: $LINEAR_WEBHOOK_SECRETLinear에서 Settings -> API -> Webhooks로 들어가 URL을 https://<public-host>/symphony/webhook/linear 형태로 등록하면 된다. Symphony는 /webhook/linear를 직접 수신하고, reverse proxy가 /symphony 접두사를 제거하는 구조를 가정한다.
외부 공개 HTTPS 엔드포인트와 reverse proxy(Caddy, Tailscale Funnel 등)는 Symphony 저장소 범위 밖에서 관리한다. 일반적인 구성은 handle_path /symphony/*를 127.0.0.1:7778로 프록시하고, Caddy 헬스체크는 GET /symphony/webhook/health에 연결하는 방식이다.
webhook.signing_secret를 비워두면 개발 모드로 동작하며, 서명 검증 없이 200을 반환하고 refresh를 수행한다. 값이 있으면 Linear-Signature HMAC-SHA256 검증에 성공한 Issue 이벤트만 refresh를 트리거한다. 검증 실패나 잘못된 payload는 재시도를 피하기 위해 항상 200으로 응답하고 로그만 남긴다.
codex.command 첫 번째 단어가 claude 또는 claude-code이면 Claude Code가 사용되고, 그 외에는 Codex가 사용된다.
# Codex (기본) — JSON-RPC 장수 프로세스
codex:
command: codex app-server
# Claude Code — process-per-turn (claude -p)
codex:
command: claudeClaude Code 동작 방식: 턴마다 claude -p 프로세스를 새로 생성한다. 이슈별 세션 ID가 워크스페이스의 .symphony/session_id에 저장되어 --session-id/--resume로 멀티턴 대화를 유지한다. --output-format stream-json --verbose로 실시간 이벤트를 스트리밍하며, --dangerously-skip-permissions로 자동 실행된다.
Claude 전용 설정 (선택):
claude:
model: claude-sonnet-4-20250514 # 모델 오버라이드 (빈 문자열이면 기본값)
append_system_prompt: "" # 시스템 프롬프트에 추가할 텍스트단일 프로젝트 실행(symphony run)에서는 --port 옵션으로 status server를 켤 수 있고, 멀티 프로젝트 데몬(symphony daemon)에서는 status_server.port 설정으로 항상 같은 API를 노출한다.
# 기본 daemon 설정(~/.config/symphony/config.yaml)의 status_server.port 사용
# live dashboard를 그리고 Ctrl+C까지 계속 갱신
symphony status
# 다른 호스트나 SSH 포트 포워딩 대상 조회
symphony status --url http://127.0.0.1:7777
# 1회 출력
symphony status --once
# 자동화용 JSON 출력
symphony status --json--url을 주지 않으면 daemon 설정 파일에서 status_server.port를 읽고, 설정 파일이 없으면 기본값 http://127.0.0.1:7777로 조회한다. 기본 text 출력은 터미널에서 live dashboard를 그리고 3초마다 새 summary를 가져와 갱신한다. --once를 주면 기존처럼 1회 출력만 하고 종료하며, --json은 항상 단발 JSON 출력이다.
원격 웹 대시보드는 이번 범위에 포함하지 않는다. 필요하면 동일한 status API를 SSH 포트 포워딩, Tailscale, reverse proxy 같은 운영 경로로 노출해 후속으로 붙일 수 있다.
| 엔드포인트 | 설명 |
|---|---|
GET /api/v1/summary |
live status CLI용 데몬 요약 상태(JSON) |
GET /api/v1/projects |
프로젝트별 상태와 실행 중 이슈 상세(JSON) |
POST /api/v1/refresh |
즉시 폴링+조정 트리거 |
이슈별 작업 디렉토리 생명주기에 실행될 스크립트를 정의한다.
hooks:
after_create: |
git clone https://github.com/myorg/myrepo.git .
npm install
before_run: |
git fetch origin
git rebase origin/main
after_run: |
git push origin HEAD
before_remove: |
echo "Cleaning up workspace"
timeout_ms: 60000스크립트 실행 시 SYMPHONY_WORKSPACE 환경변수로 현재 워크스페이스 절대경로가 전달된다.
codex.turn_sandbox_policy: workspace-write 또는 codex.thread_sandbox: workspace-write를 사용할 때 Symphony는 워크스페이스 디렉터리만 믿지 않고, 실행 직전 git rev-parse --git-dir와 --git-common-dir로 실제 git admin 경로를 해석해 codex --add-dir와 turn writableRoots로 함께 전달한다. 따라서 일반 clone과 linked worktree 모두에서 .git metadata 쓰기가 가능하고, 워크스페이스를 프로젝트 하위로 옮기는 것만으로 해결되지는 않는다.
Linear 이슈에 대한 코멘트, PR 링크 추가, 상태 전환은 에이전트가 MCP를 통해 직접 수행한다. Go 오케스트레이터는 이슈 폴링과 에이전트 디스패치만 담당한다.
tracker:
pause_states: [Plan Review, Human Review]
agent:
max_concurrent_agents_by_state:
Merging: 1
max_attempts: 3tracker.pause_states: active 상태 중 dispatch/retry/concurrency 계산에서 제외할 상태 목록. 기본값은["Human Review"].agent.max_concurrent_agents_by_state: 상태별 동시 실행 상한. 명시한 상태만 개별 quota를 적용하고, 나머지 활성 상태는agent.max_concurrent_agents전체 quota를 공유한다. 기본값은 비어 있음.agent.max_attempts: 워커 실행 최대 시도 횟수. 기본값은3.
| 변수 | 타입 | 설명 |
|---|---|---|
issue.id |
string | Linear 내부 ID |
issue.identifier |
string | 티켓 번호 (예: MY-123) |
issue.title |
string | 이슈 제목 |
issue.description |
string|null | 이슈 설명 |
issue.priority |
int|null | 우선순위 (1=긴급, 4=낮음) |
issue.state |
string | 현재 상태 |
issue.labels |
list[string] | 레이블 목록 |
issue.url |
string|null | Linear 이슈 URL |
issue.branch_name |
string|null | 연결된 브랜치 이름 |
attempt |
int | 시도 횟수 (1=최초, 2+=재시도) |
| 항목 | 기본값 |
|---|---|
polling.interval_ms |
10000 (10초) |
polling.webhook_fallback_interval_ms |
300000 (5분) |
workspace.root |
~/.symphony/workspaces |
agent.max_concurrent_agents |
10 |
agent.max_concurrent_agents_by_state |
없음 |
agent.max_attempts |
3 |
agent.max_turns |
20 |
agent.max_retry_backoff_ms |
300000 (5분) |
codex.command |
codex app-server |
codex.turn_timeout_ms |
3600000 (1시간) |
codex.stall_timeout_ms |
300000 (5분) |
claude.model |
"" (빈 문자열 = 기본 모델) |
claude.append_system_prompt |
"" |
hooks.timeout_ms |
60000 (60초) |
daemon.drain_timeout_ms |
codex.stall_timeout_ms + hooks.timeout_ms (360000, 6분) |
| 항목 | 기본값 |
|---|---|
agent.max_total_concurrent_sessions |
동적 (NumCPU() <= 2 → 1, <= 4 → 2, 그 외 NumCPU()/2, 최대 8) |
webhook.enabled |
false |
webhook.port |
7778 |
webhook.bind_address |
127.0.0.1 |
webhook.signing_secret |
없음 |
Linear webhook (`/webhook/linear`)
↓
즉시 refresh trigger
↓
활성 이슈 fetch (active_states 필터)
↑
폴백 폴링 (`webhook_fallback_interval_ms`, daemon webhook 모드일 때)
또는
일반 폴링 (`interval_ms` / `idle_interval_ms`)
↓
우선순위 정렬: priority 오름차순 → 생성일 오래된 순 → identifier 사전순
↓
dispatch 조건 확인:
- running/claimed에 없을 것
- global/per-state concurrency 여유 있을 것
- Todo 상태면 blocker가 모두 terminal일 것
↓
워크스페이스 생성 (없으면 생성 + after_create hook)
↓
before_run hook 실행
↓
에이전트 프로세스 시작 → JSON-RPC 핸드셰이크
↓
turn 실행 (최대 max_turns회)
→ 각 turn 후 이슈 상태 재확인 → terminal이면 중단
↓
after_run hook 실행
↓
재시도 예약 (정상 종료: 1초, 비정상: 지수 백오프)
| 상황 | 지연 |
|---|---|
| 정상 종료 후 continuation | 1초 |
| 비정상 종료 attempt 1 | 10초 |
| 비정상 종료 attempt 2 | 20초 |
| 비정상 종료 attempt 3 | 40초 |
| 비정상 종료 attempt 4+ | 최대 5분 |
- stall 감지: 마지막 에이전트 이벤트로부터
stall_timeout_ms초과 시 강제 종료 후 재시도 - terminal 상태: Linear에서 terminal로 전환된 이슈 → 에이전트 종료 + 워크스페이스 삭제
symphony/
├── cmd/symphony/ # CLI 진입점
├── internal/
│ ├── agent/ # 에이전트 프로세스 실행 (JSON-RPC)
│ ├── config/ # WORKFLOW.md + daemon config 파싱
│ ├── daemon/ # 멀티 프로젝트 매니저, 자동 업데이트
│ ├── filewatch/ # 파일 변경 감지
│ ├── orchestrator/ # 메인 오케스트레이션 루프
│ ├── status/ # HTTP 상태 API
│ ├── tracker/ # Linear GraphQL 클라이언트
│ ├── update/ # GitHub Releases 업데이트 체커
│ ├── version/ # 버전 정보
│ ├── webhook/ # Linear webhook 수신 서버
│ ├── workflow/ # WORKFLOW.md 로드 + 템플릿 렌더링
│ └── workspace/ # 이슈별 디렉토리 관리 + hooks
├── scripts/ # LaunchAgent plist 템플릿
├── .github/workflows/ # CI/CD (release)
└── Makefile