์๋ฆฌ์ฆ: oh-my-codex ์ํคํ ์ฒ ํด๋ถ
์ด ์๋ฆฌ์ฆ๋ OpenAI Codex CLI ํ์ฅ ๋ฐํ์์ธ oh-my-codex(OMX)์ ๋ด๋ถ ๊ตฌ์กฐ๋ฅผ ๋จ๊ณ๋ณ๋ก ํด๋ถํ๋ ๊ณผ์ ์ด๋ค.
| ํธ | ๋ด์ฉ | ํต์ฌ |
|---|---|---|
| 0ํธ | Overview | 3-Plane ์ํคํ ์ฒ, OMC์์ ์ฐจ์ด, ์ ์ฒด ํ๋ฆ |
| 1ํธ | Codex CLI Foundation | Codex CLI ์์ฒด์ ๊ตฌ์กฐ์ ํ์ฅ ํฌ์ธํธ |
| 2ํธ | OMX Integration | OMX๊ฐ Codex์ ์ด๋ป๊ฒ ์ฐ๊ฒฐ๋๋ |
| 3ํธ | Skill System | ์ํฌํ๋ก์ฐ๋ฅผ ์ด๋ป๊ฒ ์ ์ํ๋ |
| 4ํธ | Prompt & Agent System | ์์ด์ ํธ๋ ๋ญ๊ณ ์ด๋ป๊ฒ ์ ํ๋๋ |
| 5ํธ | MCP Servers | ์ด๋ค MCP ๋๊ตฌ๋ฅผ ์ธ ์ ์๋ |
| 6ํธ (๋ณธ๋ฌธ) | State & Lifecycle | ์ํ๋ฅผ ์ด๋ป๊ฒ ์ ์งํ๋ |
| 7ํธ | Team Orchestration | Team ๋ชจ๋๋ ์ด๋ป๊ฒ ๋์ํ๋ |
| 8ํธ | Native & Spark | Rust ๋ค์ดํฐ๋ธ ๋๊ตฌ๋ ๋ญ๊ฐ |
- State & Lifecycle์ OMX์ ๋ชจ๋ ์ํ ๊ด๋ฆฌ, ์ธ์ ์๋ช ์ฃผ๊ธฐ, ๊ณํ ๊ฒ์ดํธ ์ ๋ต
.omx/๋๋ ํ ๋ฆฌ์ ๋ชจ๋ ์ํยท์ธ์ ๋ฉํยท๊ณํ ์ฐ์ถ๋ฌผยท๋ก๊ทธ๋ฅผ ์์ํํ๋ฉฐ, PID ๊ธฐ๋ฐ stale ๊ฐ์ง์ atomic write๋ก ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ๋ ์ํ ์ธํ๋ผ- 5ํธ์ MCP ์๋ฒ๊ฐ โ์ด๋ป๊ฒ ์ฝ๊ณ ์ฐ๋์งโ ๋ฅผ ๋ค๋ค๋ค๋ฉด, ์ด ํํธ๋ โ์ธ์ , ๋ฌด์์, ์ด๋ค ๊ท์น์ผ๋กโ ์ํ๋ฅผ ๊ด๋ฆฌํ๋์ง๋ฅผ ๋ค๋ฃจ๋ ๊ตฌ์กฐ
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- 5ํธ์์ MCP ์๋ฒ๋ก ์ํ๋ฅผ ์ฝ๊ณ ์ธ ์ ์๊ฒ ๋์์
- ํ์ง๋ง โ์ธ์ ์ํ๋ฅผ ๊ธฐ๋กํ๊ณ , ๋ชจ๋ ์ ํ์ ์ด๋ป๊ฒ ๋๋ฉฐ, ์ธ์ ๊ฐ ์ ๋ณด๋ฅผ ์ด๋ป๊ฒ ๋ณด์กดํ๋์งโ ๊ฐ ๋น ์ ธ ์์
- ์ด ํํธ๋ ๋ชจ๋ ์ํ ๋จธ์ , ๋ฐฐํ์ /ํฉ์ฑ ๊ท์น, Ralph planning gate, ์ธ์ ๊ด๋ฆฌ๋ฅผ ๋ค๋ฃจ์ด ๊ทธ ๊ฐ๊ทน์ ๋ฉ์
AS-IS (OMC โ Hook ๊ธฐ๋ฐ ์ข ๋ฃ ์ฐจ๋จ)
sequenceDiagram autonumber participant CC as Claude Code participant SH as persistent-mode.mjs (Stop Hook) participant ST as .omc/state/ CC->>CC: ์์ ์๋ฃ ์๋ CC->>SH: Stop Hook ์ด๋ฒคํธ ๋ฐ์ SH->>ST: ํ์ฑ ๋ชจ๋ ํ์ธ alt ํ์ฑ ๋ชจ๋ ์กด์ฌ SH-->>CC: ์ข ๋ฃ ์ฐจ๋จ (stdout JSON ์ฃผ์ ) Note over CC: Hook์ด ๊ฒฐ์ ๋ก ์ ์ผ๋ก ์ข ๋ฃ๋ฅผ ์ฐจ๋จ else ํ์ฑ ๋ชจ๋ ์์ SH-->>CC: ์ข ๋ฃ ํ์ฉ end
TO-BE (OMX โ AGENTS.md Continuation ์ง์นจ)
sequenceDiagram autonumber participant CX as Codex CLI (LLM) participant AM as AGENTS.md participant ST as .omx/state/ CX->>CX: ์์ ์๋ฃ ์๋ CX->>AM: Continuation ์ง์นจ ํ์ธ Note over AM: "no pending work, features working,<br/>tests passing, zero known errors,<br/>verification evidence collected. If not, continue." CX->>ST: state_read(mode) โ ํ์ฑ ๋ชจ๋ ํ์ธ alt ๋ฏธ์๋ฃ ์์ ์กด์ฌ CX->>CX: LLM์ด ์๋ฐ์ ์ผ๋ก ์์ ๊ณ์ Note over CX: ํ๋ฅ ๋ก ์ โ LLM ํ๋จ์ ์์กด else ์๋ฃ ์กฐ๊ฑด ์ถฉ์กฑ CX->>ST: state_write(active: false, completed_at: ...) end
ํต์ฌ ์ฐจ์ด: OMC๋ Stop Hook์ผ๋ก ๊ฒฐ์ ๋ก ์ ์ผ๋ก ์ข ๋ฃ๋ฅผ ์ฐจ๋จํ์ง๋ง, OMX๋ AGENTS.md Continuation ์ง์นจ์ผ๋ก LLM์ด ์๋ฐ์ ์ผ๋ก ๊ณ์ํ ์ง ํ๋จํ๋ค. ์ ๊ณ์ธต์ด ํ๋ฅ ๋ก ์ ์ธ OMX ์ํคํ ์ฒ์ ์ผ๊ด๋ ์ค๊ณ๋ค.
.omx/ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
.omx/
โโโ state/ # ๋ชจ๋ยท์ธ์
์ํ
โ โโโ {mode}-state.json # ๋ชจ๋๋ณ ์ํ (autopilot, ralph, team ๋ฑ)
โ โโโ session.json # ํ์ฌ ์ธ์
๋ฉํ๋ฐ์ดํฐ
โ โโโ hud-state.json # HUD ๋ฉํธ๋ฆญ (์ธ์
์์ ์ ๋ฆฌ์
)
โ โโโ skill-active-state.json # ํ์ฌ ํ์ฑ ์คํฌ ์ถ์
โ โโโ ralph-progress.json # Ralph ์คํ ๋ ์
โ โโโ sessions/
โ โโโ {sessionId}/ # ์ธ์
์ค์ฝํ ์ํ
โ โโโ {mode}-state.json
โโโ plans/ # ๊ณํ ์ฐ์ถ๋ฌผ (Ralph planning gate)
โ โโโ prd-*.md # PRD ๋ฌธ์
โ โโโ test-spec-*.md # ํ
์คํธ ์คํ
โโโ specs/ # ์คํ ํ์ผ
โ โโโ deep-interview-*.md # Deep interview ์ฐ์ถ๋ฌผ
โโโ logs/ # ์ธ์
ํ์คํ ๋ฆฌยท์ผ์ผ ๋ก๊ทธ
โ โโโ session-history.jsonl # ์ข
๋ฃ๋ ์ธ์
์์นด์ด๋ธ
โ โโโ omx-YYYY-MM-DD.jsonl # ์ผ์ผ ์ด๋ฒคํธ ๋ก๊ทธ
โ โโโ turns-*.jsonl # ํด ๋ก๊ทธ (trace ์๋ฒ์ฉ)
โโโ metrics.json # ์ธ์
๋ฉํธ๋ฆญ (ํ ํฐ, ํด ์)
โโโ project-memory.json # ํ๋ก์ ํธ ๋ฉ๋ชจ๋ฆฌ (ํฌ๋ก์ค ์ธ์
)
โโโ notepad.md # ๋
ธํธํจ๋
โโโ setup-scope.json # ์ค์น scope ๊ธฐ๋ก (user/project)
โโโ .gitignore # .omx/ ์ ์ฒด๋ฅผ gitignore ์ฒ๋ฆฌ
omx setup ์ state/, plans/, logs/ ๋๋ ํ ๋ฆฌ๊ฐ ์์ฑ๋๊ณ , .gitignore์ .omx/ ์ํธ๋ฆฌ๊ฐ ์ถ๊ฐ๋๋ค.
๋ชจ๋ ์ํ ๋จธ์ โ ๋ฐฐํ์ vs ํฉ์ฑ
7๊ฐ ๋ชจ๋ ์ ์ (์์ค: src/modes/base.ts)
export type ModeName =
'autopilot' | 'autoresearch' | 'ralph' | 'ultrawork' |
'team' | 'ultraqa' | 'ralplan';๋ฐฐํ์ (Exclusive) ๋ชจ๋ โ ๋์ ์คํ ๋ถ๊ฐ
const EXCLUSIVE_MODES: ModeName[] = ['autopilot', 'autoresearch', 'ralph', 'ultrawork'];๋ฐฐํ์ ๋ชจ๋ ์์ ์ assertModeStartAllowed()๊ฐ ๋ค๋ฅธ ๋ฐฐํ์ ๋ชจ๋์ ํ์ฑ ์ํ๋ฅผ ํ์ธํ๋ค. ์ถฉ๋ ์ "Cannot start {mode}: {other} is already active. Run cancel first." ์๋ฌ๋ฅผ ๋์ง๋ค.
| ๋ชจ๋ | ๋ฐฐํ/ํฉ์ฑ | ๋น๊ณ |
|---|---|---|
autopilot | ๋ฐฐํ์ | ์ ์ฒด ์๋ํ ํ์ดํ๋ผ์ธ |
ralph | ๋ฐฐํ์ | ๊ฒ์ฆ ๋ฃจํ (๋ด๋ถ์ ์ผ๋ก ultrawork ์์ ๊ฐ๋ฅ) |
ultrawork | ๋ฐฐํ์ | ๋ณ๋ ฌ ์คํ |
autoresearch | ๋ฐฐํ์ | ์๋ ๋ฆฌ์์น |
team | ํฉ์ฑ | N worker ๋ณ๋ ฌ ์กฐ์จ |
ultraqa | ํฉ์ฑ | QA ์ฌ์ดํด |
ralplan | ํฉ์ฑ | ํฉ์ ๊ธฐ๋ฐ ๊ณํ (planning gate) |
3ํธ Skill Composition๊ณผ์ ๊ด๊ณ: ralph๊ฐ ultrawork๋ฅผ โ๊ฐ์ธ๋โ 3๊ณ์ธต ํฉ์ฑ์ ๋ชจ๋ ์์ค์ ๋์ ํ์ฑ์ด ์๋๋ค. Ralph ๋ชจ๋๊ฐ ํ์ฑ์ผ ๋ ๋ด๋ถ์ ์ผ๋ก ultrawork์ ๋ณ๋ ฌ ์์์ ์ฌ์ฉํ๋ ๊ฒ์ด์ง, ๋ ๋ชจ๋๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ๋์์ active ์ํ๊ฐ ๋๋ ๊ฒ์ด ์๋๋ค.
์ํ ์ ์ด ๋ค์ด์ด๊ทธ๋จ
stateDiagram-v2 [*] --> inactive inactive --> active: state_write(active: true) state active { [*] --> starting starting --> executing: phase ๋ณ๊ฒฝ executing --> verifying: ์์ ์๋ฃ, ๊ฒ์ฆ ์์ verifying --> fixing: ๊ฒ์ฆ ์คํจ fixing --> executing: ์์ ํ ์ฌ์คํ verifying --> complete: ๊ฒ์ฆ ํต๊ณผ executing --> failed: ์๋ฌ ๋ฐ์ fixing --> failed: ์๋ฌ ๋ฐ์ } active --> completed: state_write(active: false, completed_at: ...) active --> cancelled: cancel ์คํฌ ์คํ completed --> [*] cancelled --> [*]
๋ชจ๋ ๋ผ์ดํ์ฌ์ดํด ๊ท์น
AGENTS.md <state_management> ์น์
์ ๋ช
์๋ 4๋จ๊ณ ํ์ ๊ท์น:
| ๋จ๊ณ | ๋์ | ์์ |
|---|---|---|
| 1. ์์ | state_write โ ๋ชจ๋ ์ํ ๊ธฐ๋ก | state_write({mode: "ralph", active: true, started_at: ...}) |
| 2. ๋ณ๊ฒฝ | ํ์ด์ฆ/์ดํฐ๋ ์ด์ ๋ณ๊ฒฝ ์ ์ ๋ฐ์ดํธ | state_write({current_phase: "verifying", iteration: 2}) |
| 3. ์๋ฃ | completed_at ๋งํน, active: false | state_write({active: false, completed_at: "2026-..."}) |
| 4. ์ทจ์ | ์ ๋ฆฌ ํ ์ํ ํด๋ฆฌ์ด | state_clear({mode: "ralph"}) |
Ralph Phase Contract (์์ค: src/ralph/contract.ts)
Ralph ๋ชจ๋๋ 7๊ฐ ์ ๊ท phase๋ฅผ ๊ฐ์ง๋ฉฐ, ๋ ๊ฑฐ์ ๋ณ์นญ์ด ์๋ ์ ๊ทํ๋๋ค.
export const RALPH_PHASES = [
'starting', 'executing', 'verifying', 'fixing',
'complete', 'failed', 'cancelled'
] as const;| ๋ ๊ฑฐ์ ๋ณ์นญ | ์ ๊ทํ ๋์ |
|---|---|
start, started | starting |
execution, execute | executing |
verify, verification | verifying |
fix | fixing |
completed | complete |
fail, error | failed |
cancel | cancelled |
ํฐ๋ฏธ๋ phase: complete, failed, cancelled โ ์ด ์ํ์ ๋๋ฌํ๋ฉด ๋ชจ๋๊ฐ ๋นํ์ฑํ๋๋ค.
Mode Runtime Context (์์ค: src/state/mode-state-context.ts)
๋ชจ๋๊ฐ ํ์ฑํ๋ ๋ TMUX pane ID๋ฅผ ์๋ ์บก์ฒํ์ฌ ์ํ์ ์ ์ฅํ๋ค.
export function withModeRuntimeContext<T>(existing, next, options?) {
// active ์ ํ ์ ํ๊ฒฝ๋ณ์์์ TMUX_PANE ์บก์ฒ
// ์ต์ด 1ํ๋ง ์ ์ฅ (์ดํ ๋ฎ์ด์ฐ์ง ์์)
// tmux_pane_id, tmux_pane_set_at ๊ธฐ๋ก
}์ด ์ ๋ณด๋ team ๋ชจ๋์์ ์ด๋ค pane์ด ์ด๋ค ๋ชจ๋๋ฅผ ์คํ ์ค์ธ์ง ์ถ์ ํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
Session ๊ด๋ฆฌ (์์ค: src/hooks/session.ts)
์ธ์ ์๋ช ์ฃผ๊ธฐ
sequenceDiagram autonumber participant H as ์ฌ๋ participant CLI as omx launch participant SS as session.ts participant SF as .omx/state/session.json participant LG as .omx/logs/ H->>CLI: omx launch CLI->>SS: writeSessionStart(cwd, sessionId) SS->>SF: session.json ์์ฑ (pid, started_at, cwd) SS->>LG: omx-YYYY-MM-DD.jsonl์ session_start ๊ธฐ๋ก SS->>SS: resetSessionMetrics() โ metrics.json ์ด๊ธฐํ Note over CLI: ์ธ์ ์งํ ์ค... CLI->>SS: writeSessionEnd(cwd, sessionId) SS->>LG: session-history.jsonl์ ์์นด์ด๋ธ (started_at, ended_at) SS->>SF: session.json ์ญ์ SS->>LG: omx-YYYY-MM-DD.jsonl์ session_end ๊ธฐ๋ก
SessionState ์ธํฐํ์ด์ค
export interface SessionState {
session_id: string;
started_at: string;
cwd: string;
pid: number;
platform?: NodeJS.Platform;
pid_start_ticks?: number; // Linux: /proc/PID/stat ํ๋ 20
pid_cmdline?: string; // Linux: /proc/PID/cmdline
}Stale ์ธ์ ๊ฐ์ง โ PID ๊ธฐ๋ฐ (์๊ฐ ์ ํ ์์)
export function isSessionStale(state: SessionState): boolean {
// 1. PID liveness: process.kill(pid, 0)
// 2. Linux ์ ์ฉ: /proc/PID/stat์ start ticks ๋น๊ต
// โ PID๊ฐ ์ฌ์ฌ์ฉ๋์๋์ง ํ์ธ (๊ฐ์ ๋ฒํธ, ๋ค๋ฅธ ํ๋ก์ธ์ค)
// 3. macOS/Windows: PID liveness๋ง ํ์ธ
}OMC์์ ์ฐจ์ด: OMC๋ 2์๊ฐ ํ์์์์ผ๋ก stale์ ํ๋จํ์ง๋ง, OMX๋ PID ์์กด ์ฌ๋ถ๋ง ํ์ธํ๋ค. ์์ค ์ฃผ์: โNo age-based threshold: staleness is determined by PID liveness/identity. Long-running sessions (>2h) are legitimate and should not be reaped.โ
์ธ์
๋ฉํธ๋ฆญ ์ด๊ธฐํ (resetSessionMetrics)
์ธ์
์์ ์ .omx/metrics.json์ ๋ฆฌ์
ํ๋ค:
{
"total_turns": 0,
"session_turns": 0,
"last_activity": "2026-03-19T...",
"session_input_tokens": 0,
"session_output_tokens": 0,
"session_total_tokens": 0
}Ralph Planning Gate (์์ค: src/planning/artifacts.ts)
๊ฒ์ดํธ ์กฐ๊ฑด
const PRD_PATTERN = /^prd-.*\.md$/i;
const TEST_SPEC_PATTERN = /^test-?spec-.*\.md$/i;
export function isPlanningComplete(artifacts: PlanningArtifacts): boolean {
return artifacts.prdPaths.length > 0 && artifacts.testSpecPaths.length > 0;
}์์ชฝ ๋ชจ๋ ์กด์ฌํด์ผ ๊ฒ์ดํธ ํด์ :
.omx/plans/prd-*.mdโ ์ต์ 1๊ฐ.omx/plans/test-spec-*.md(๋๋testspec-*.md) โ ์ต์ 1๊ฐ
๊ฒ์ดํธ ๋์ ํ๋ฆ
graph TD RS["Ralph ํ์ฑ ์ํ"] RS --> CHECK["isPlanningComplete() ํ์ธ"] CHECK --> |"PRD + test-spec ๋ชจ๋ ์กด์ฌ"| UNLOCK["UNLOCKED โ ๊ตฌํ ์์ ํ์ฉ"] CHECK --> |"ํ๋๋ผ๋ ์์"| BLOCK["BLOCKED โ ๊ตฌํ ์ฐจ๋จ"] BLOCK --> RP["$ralplan์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ<br/>๊ณํ ๋จผ์ ์ํ"] RP --> |"๊ณํ ์ฐ์ถ๋ฌผ ์์ฑ"| CHECK style UNLOCK fill:#d4edda style BLOCK fill:#f8d7da
์น์ธ๋ ์คํ ํํธ ์ถ์ถ
readApprovedExecutionLaunchHint()๊ฐ ์ต์ PRD ๋งํฌ๋ค์ด์์ ์คํ ๋ช
๋ น์ด๋ฅผ ํ์ฑํ๋ค:
// PRD ๋ด ํจํด ๋งค์นญ: "omx team 3:executor refactor API"
// โ { workerCount: 3, agentRole: "executor", taskDescription: "refactor API" }์ด๋ฅผ ํตํด planning gate ํต๊ณผ ํ ์๋์ผ๋ก ์ ์ ํ ์คํ ๋ชจ๋๋ฅผ ์์ํ ์ ์๋ค.
Skill Active State โ ์คํฌ ํ์ฑํ ์ถ์
skill-active-state.json (์์ค: src/hooks/keyword-detector.ts)
export interface SkillActiveState {
version: 1;
active: boolean;
skill: string; // ํ์ฑ ์คํฌ ์ด๋ฆ
keyword: string; // ํธ๋ฆฌ๊ฑฐํ ํค์๋
phase: SkillActivePhase; // 'planning' | 'executing' | 'reviewing' | 'completing'
activated_at: string;
updated_at: string;
source: 'keyword-detector';
session_id?: string;
input_lock?: DeepInterviewInputLock; // Deep Interview ์ ์ฉ
}recordSkillActivation()์ด ์ฌ์ฉ์ ์
๋ ฅ์์ ํค์๋๋ฅผ ๊ฐ์งํ ๋๋ง๋ค ์ด ํ์ผ์ ์
๋ฐ์ดํธํ๋ค. ๊ฐ์ ์คํฌ/ํค์๋๊ฐ ์ด๋ฏธ ํ์ฑ์ด๋ฉด activated_at์ ๋ณด์กดํ๊ณ , ์ ์คํฌ์ด๋ฉด ์ ํ์์คํฌํ๋ฅผ ๊ธฐ๋กํ๋ค.
Deep Interview Input Lock
3ํธ์์ ๋ค๋ฃฌ input lock์ ์ํ ๊ด๋ฆฌ ์ธก๋ฉด:
export interface DeepInterviewInputLock {
active: boolean;
scope: 'deep-interview-auto-approval';
acquired_at: string;
released_at?: string;
exit_reason?: 'success' | 'error' | 'abort' | 'handoff';
blocked_inputs: string[]; // ['yes', 'y', 'proceed', 'continue', 'ok', ...]
message: string;
}- ํ๋: deep-interview ์คํฌ ํ์ฑํ ์
createDeepInterviewInputLock() - ํด์ : cancel ํค์๋ ๊ฐ์ง ์ ๋๋ ์ธํฐ๋ทฐ ์๋ฃ ์
releaseDeepInterviewInputLock(exit_reason) - ์ํ ํ์ผ:
skill-active-state.json์input_lockํ๋์ ์์ํ
OMC ์ํ ๊ด๋ฆฌ์์ ๋น๊ต
| ํญ๋ชฉ | OMC | OMX |
|---|---|---|
| ์ํ ๋๋ ํ ๋ฆฌ | .omc/state/ | .omx/state/ |
| ์ข ๋ฃ ์ฐจ๋จ | persistent-mode.mjs Stop Hook (๊ฒฐ์ ๋ก ์ ) | AGENTS.md Continuation ์ง์นจ (ํ๋ฅ ๋ก ์ ) |
| Stale ๊ฐ์ง | 2์๊ฐ ํ์์์ | PID liveness (์๊ฐ ์ ํ ์์) |
| Stale ํ๋ซํผ | ๋ฒ์ฉ | Linux: /proc/PID/stat ํ๋ก์ธ์ค ๋์ผ์ฑ ๊ฒ์ฆ |
| Notepad | .omc/notepad.md (3์น์
) | omx_memory MCP ์๋ฒ์ notepad_* ๋๊ตฌ |
| Project Memory | .omc/project-memory.json | omx_memory MCP ์๋ฒ์ project_memory_* ๋๊ตฌ |
| ์ธ์ ์ค์ฝํ | .omc/state/sessions/{id}/ | .omx/state/sessions/{id}/ (๋์ผ ํจํด) |
| ๋ชจ๋ ๋ฐฐํ์ฑ | ์คํฌ ์์ค ๊ด๋ฆฌ | assertModeStartAllowed() ์ฝ๋ ์์ค ๊ฐ์ |
| ์ํ ์์์ฑ | ์ง์ ํ์ผ ์ฐ๊ธฐ | atomic write (tmp + rename) + write-lock queue |
| Planning gate | ์์ | isPlanningComplete() โ PRD + test-spec ํ์ |