feat: separate codex live and automation context

This commit is contained in:
2026-04-24 08:53:15 +09:00
parent f2d6310efa
commit 91230304e0
9 changed files with 142 additions and 60 deletions

View File

@@ -8,6 +8,7 @@
- 문서 정리나 Codex 작업 시 Git 원격 동기화, 브랜치 운영, 자동 merge 흐름은 기본 전제로 사용하지 않습니다.
- `Codex Live`, 일반 채팅, 일반 작업메모 반영 요청은 **현재 프로젝트 루트의 로컬 `main` 작업본을 바로 수정**하는 방식으로 처리합니다.
- 자동화 작업메모도 별도 브랜치 흐름을 이 문서에 고정하지 않고, 실제 운영 설정과 요청 문맥을 기준으로 처리합니다.
- 자동화와 `Codex Live`는 별개로 취급하며, 자동화는 선택된 자동화 유형 context만 우선 참조합니다.
- Git 관련 작업은 사용자가 명시적으로 요청할 때만 수행합니다.
## 1. 작업일지

View File

@@ -4,7 +4,7 @@
Plan 자동화 기능의 데이터 구조, API 연동 방식, release 검수 연계 방식을 정리합니다.
현재 문서는 자동화 브랜치 전략 자체를 고정 규칙으로 설명하지 않습니다. 자동화 처리 방식은 실제 서버 설정, 요청 문맥, 현재 운영 규칙을 함께 확인해 판단합니다. 반면 `Codex Live`나 일반 수동 요청은 로컬 작업본 기준으로 처리합니다.
현재 문서는 자동화 브랜치 전략 자체를 고정 규칙으로 설명하지 않습니다. 자동화 처리 방식은 실제 서버 설정, 요청 문맥, 현재 운영 규칙을 함께 확인해 판단합니다. 자동화와 `Codex Live`는 별개이며, 자동화 실행기는 선택된 자동화 유형의 context만 우선 참조합니다. 반면 `Codex Live`나 일반 수동 요청은 로컬 작업본 기준으로 처리합니다.
## 구현 위치
@@ -38,6 +38,12 @@ Plan 자동화 기능의 데이터 구조, API 연동 방식, release 검수 연
기존 저장값인 `plan_registration`, `general_development`는 서버에서 각각 `plan`, `auto_worker`로 정규화합니다.
자동화 context 해석 규칙:
- 자동화 실행 시 기본 문맥은 선택된 자동화 유형 description/context만 사용
- `Codex Live` 채팅 문맥, 일반 채팅 문맥은 자동화 기본 context로 섞지 않음
- 추가 지시가 필요하면 요청 본문에 명시적으로 적어 전달
## API 연동 방식
기본 API 베이스 URL 규칙:

View File

@@ -59,6 +59,8 @@ npm run server-command:runner
현재 운영 기준에서는 `Codex Live`, 일반 채팅, 일반 작업메모 반영 요청은 **현재 프로젝트 루트의 로컬 작업본을 직접 기준으로 처리**합니다.
`Codex Live``Plan` 자동화는 별개입니다. `Codex Live`는 채팅 유형과 현재 화면 문맥을 기준으로 동작하고, 자동화 유형 context를 기본 문맥으로 섞지 않습니다.
브라우저 기준 접속 확인, 화면 검증, 외부 도메인 테스트는 **`https://test.sm-home.cloud/`를 기본 작업 도메인으로 사용**합니다. 별도 요청이 없는 한 `sm-home.cloud``rel.sm-home.cloud`는 기본 검증 대상으로 삼지 않습니다.
채팅에서 파일, 문서, 이미지, 코드 같은 리소스를 제공할 때의 기본 공개 경로는 `public/.codex_chat/<chat-session-id>/resource/...`입니다. Codex가 원본 파일 경로만 답해도 서버가 이 위치로 세션 전용 사본을 만들고, 채팅에는 공개 URL을 다시 적어 줍니다.
@@ -71,6 +73,8 @@ npm run server-command:runner
현재 운영 기준에서 자동화 작업메모의 세부 Git 절차는 이 문서에 고정하지 않습니다. 상태 전이와 실제 처리 흐름은 worker 구현, 환경 변수, 현재 운영 정책을 함께 확인해야 합니다.
자동화 실행기는 선택된 자동화 유형의 description/context만 우선 참조합니다. `Codex Live`나 일반 채팅 문맥은 자동화 기본 context로 사용하지 않습니다.
안전 조건:
- Git worktree가 깨끗해야 동작

View File

@@ -60,7 +60,8 @@ export const DEFAULT_AUTOMATION_TYPES: AutomationTypeRecord[] = [
{
id: 'auto_worker',
name: 'autoWorker',
description: '자동화 작업메모로 처리하며, 세부 절차는 현재 운영 설정을 따릅니다.',
description:
'자동화 작업메모로 처리합니다.\n\n## context 사용 규칙\n- 자동화 실행기는 선택된 자동화 유형의 context만 우선 참조합니다.\n- Codex Live 문맥이나 일반 채팅 문맥은 자동화 기본 context로 섞지 않습니다.\n\n## 처리 원칙\n- 세부 절차는 현재 운영 설정을 따릅니다.',
behaviorType: 'auto_worker',
enabled: true,
updatedAt: '2026-04-23T00:00:00.000Z',

View File

@@ -14,8 +14,9 @@ test('buildBoardPostPlanNote formats automation work memo with clear sections',
'- 게시판 제목: 알림 개선',
'- 메모 출처: board_posts 자동화 접수',
'- 선택 자동화 유형: 자동화 메모',
'- 자동화 처리 원칙: 아래 자동화 유형 context만 우선 참조하고, Codex Live 문맥은 섞지 않습니다.',
'',
'## 선택된 유형 context',
'## 자동화 유형 context',
'## 처리 기준\n- 선택된 유형의 context를 먼저 읽습니다.',
'',
'## 요청 본문',
@@ -36,8 +37,9 @@ test('buildBoardPostPlanNote keeps context section even when automation type des
'- 게시판 제목: 작업',
'- 메모 출처: board_posts 자동화 접수',
'- 선택 자동화 유형: 빈 context 유형',
'- 자동화 처리 원칙: 아래 자동화 유형 context만 우선 참조하고, Codex Live 문맥은 섞지 않습니다.',
'',
'## 선택된 유형 context',
'## 자동화 유형 context',
'선택된 자동화 유형 context 없음',
'',
'## 요청 본문',

View File

@@ -91,8 +91,9 @@ export function buildBoardPostPlanNote(title: string, content: string, automatio
`- 게시판 제목: ${normalizedTitle}`,
'- 메모 출처: board_posts 자동화 접수',
normalizedAutomationTypeName ? `- 선택 자동화 유형: ${normalizedAutomationTypeName}` : null,
'- 자동화 처리 원칙: 아래 자동화 유형 context만 우선 참조하고, Codex Live 문맥은 섞지 않습니다.',
'',
'## 선택된 유형 context',
'## 자동화 유형 context',
normalizedAutomationContext || '선택된 자동화 유형 context 없음',
'',
'## 요청 본문',

View File

@@ -1432,6 +1432,7 @@ function buildAgenticCodexPrompt(
return [
'당신은 이 저장소에서 Codex Live 요청을 처리하는 실제 Codex 실행기입니다.',
'Codex Live는 Plan 자동화와 별개입니다. 자동화 유형 context를 Codex Live 기본 문맥으로 섞지 마세요.',
`저장소 루트(main_project): ${repoPath}`,
'반드시 저장소 루트의 AGENTS.md를 먼저 읽고 그 규칙을 따르세요.',
'가능한 작업 범위:',
@@ -1457,6 +1458,7 @@ function buildAgenticCodexPrompt(
`- chatTypeLabel: ${context?.chatTypeLabel ?? '없음'}`,
`- chatTypeDescription: ${context?.chatTypeDescription ?? '없음'}`,
'- 답변 스타일과 기본 문맥은 반드시 채팅 유형 정보만 기준으로 적용하세요.',
'- Plan 자동화용 자동화 유형 context가 있더라도 Codex Live에서는 별도 요청이 없는 한 참조하지 마세요.',
'',
'참고 화면 정보:',
`- pageTitle: ${context?.pageTitle ?? '없음'}`,

View File

@@ -3,12 +3,14 @@ import {
BellOutlined,
ClockCircleOutlined,
CopyOutlined,
DownOutlined,
FileMarkdownOutlined,
LoadingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
ProfileOutlined,
ReloadOutlined,
RightOutlined,
SettingOutlined,
} from '@ant-design/icons';
import {
@@ -883,6 +885,7 @@ export function MainHeader({
const navigate = useNavigate();
const location = useLocation();
const [settingsOpen, setSettingsOpen] = useState(false);
const [automationGroupExpanded, setAutomationGroupExpanded] = useState(false);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const [activeSettingsModal, setActiveSettingsModal] = useState<SettingsModalKey>('appSettings');
const [activeAppSettingsCategory, setActiveAppSettingsCategory] = useState<AppSettingsCategoryKey>('automation');
@@ -958,6 +961,8 @@ export function MainHeader({
const workServerPendingUpdateCount =
workServerStatus && (workServerStatus.updateAvailable || workServerStatus.buildRequired) ? 1 : 0;
const totalPendingUpdateCount = testServerPendingUpdateCount + prodServerPendingUpdateCount + workServerPendingUpdateCount;
const totalAutomationShortcutCount =
planShortcutCounts.working + planShortcutCounts.releasePendingMain + planShortcutCounts.automationFailed;
const settingsStatusClassName =
totalPendingUpdateCount >= 2
? 'app-header__status-dot--inactive'
@@ -2621,68 +2626,20 @@ export function MainHeader({
const settingsMenu = (
<div className="app-header__settings-menu">
{hasAccess ? (
<>
<div className="app-header__settings-group">
<button
type="button"
className="app-header__settings-item"
aria-expanded={automationGroupExpanded}
onClick={() => {
setSettingsOpen(false);
onOpenPlanQuickFilter('working');
}}
>
<span className="app-header__settings-icon">
<ProfileOutlined />
<span
className={`app-header__status-dot ${
planShortcutCounts.working > 0
? 'app-header__status-dot--active'
: 'app-header__status-dot--inactive'
}`}
aria-hidden="true"
/>
</span>
<span className="app-header__settings-label">
<Text type="secondary"> {planShortcutCounts.working}</Text>
</span>
</button>
<button
type="button"
className="app-header__settings-item"
onClick={() => {
setSettingsOpen(false);
onOpenPlanQuickFilter('release-pending-main');
}}
>
<span className="app-header__settings-icon">
<ProfileOutlined />
<span
className={`app-header__status-dot ${
planShortcutCounts.releasePendingMain > 0
? 'app-header__status-dot--warning'
: 'app-header__status-dot--inactive'
}`}
aria-hidden="true"
/>
</span>
<span className="app-header__settings-label">
release
<Text type="secondary"> {planShortcutCounts.releasePendingMain}</Text>
</span>
</button>
<button
type="button"
className="app-header__settings-item"
onClick={() => {
setSettingsOpen(false);
onOpenPlanQuickFilter('automation-failed');
setAutomationGroupExpanded((current) => !current);
}}
>
<span className="app-header__settings-icon">
<ReloadOutlined />
<span
className={`app-header__status-dot ${
planShortcutCounts.automationFailed > 0
totalAutomationShortcutCount > 0
? 'app-header__status-dot--warning'
: 'app-header__status-dot--inactive'
}`}
@@ -2690,11 +2647,93 @@ export function MainHeader({
/>
</span>
<span className="app-header__settings-label">
<Text type="secondary"> {planShortcutCounts.automationFailed}</Text>
<Text type="secondary"> {totalAutomationShortcutCount}</Text>
</span>
<span className="app-header__settings-group-arrow" aria-hidden="true">
{automationGroupExpanded ? <DownOutlined /> : <RightOutlined />}
</span>
</button>
</>
{automationGroupExpanded ? (
<div className="app-header__settings-group-children">
<button
type="button"
className="app-header__settings-item app-header__settings-item--nested"
onClick={() => {
setSettingsOpen(false);
setAutomationGroupExpanded(false);
onOpenPlanQuickFilter('working');
}}
>
<span className="app-header__settings-icon">
<ProfileOutlined />
<span
className={`app-header__status-dot ${
planShortcutCounts.working > 0
? 'app-header__status-dot--active'
: 'app-header__status-dot--inactive'
}`}
aria-hidden="true"
/>
</span>
<span className="app-header__settings-label">
<Text type="secondary"> {planShortcutCounts.working}</Text>
</span>
</button>
<button
type="button"
className="app-header__settings-item app-header__settings-item--nested"
onClick={() => {
setSettingsOpen(false);
setAutomationGroupExpanded(false);
onOpenPlanQuickFilter('release-pending-main');
}}
>
<span className="app-header__settings-icon">
<ProfileOutlined />
<span
className={`app-header__status-dot ${
planShortcutCounts.releasePendingMain > 0
? 'app-header__status-dot--warning'
: 'app-header__status-dot--inactive'
}`}
aria-hidden="true"
/>
</span>
<span className="app-header__settings-label">
release
<Text type="secondary"> {planShortcutCounts.releasePendingMain}</Text>
</span>
</button>
<button
type="button"
className="app-header__settings-item app-header__settings-item--nested"
onClick={() => {
setSettingsOpen(false);
setAutomationGroupExpanded(false);
onOpenPlanQuickFilter('automation-failed');
}}
>
<span className="app-header__settings-icon">
<ReloadOutlined />
<span
className={`app-header__status-dot ${
planShortcutCounts.automationFailed > 0
? 'app-header__status-dot--warning'
: 'app-header__status-dot--inactive'
}`}
aria-hidden="true"
/>
</span>
<span className="app-header__settings-label">
<Text type="secondary"> {planShortcutCounts.automationFailed}</Text>
</span>
</button>
</div>
) : null}
</div>
) : null}
<button
type="button"

View File

@@ -174,10 +174,23 @@
gap: 8px;
}
.app-header__settings-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.app-header__settings-item:hover {
background: #f3f7ff;
}
.app-header__settings-item--nested {
margin-left: 12px;
min-width: 0;
padding-left: 14px;
background: #f8fbff;
}
.app-header__settings-icon {
position: relative;
display: inline-flex;
@@ -241,10 +254,23 @@
}
.app-header__settings-label {
display: inline-flex;
align-items: center;
gap: 4px;
flex: 1;
font-size: 14px;
font-weight: 600;
}
.app-header__settings-group-arrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
color: #64748b;
font-size: 12px;
}
.app-header__update-progress {
display: flex;
flex-direction: column;