chore: update plan automation and chat status UI

This commit is contained in:
2026-04-23 20:27:40 +09:00
parent 6e863feafd
commit 346d4c2208
26 changed files with 317 additions and 74 deletions

View File

@@ -57,7 +57,15 @@ npm run server-command:runner
소스 수정이 필요하면 **현재 실행 환경에서 실제로 연결된 `main_project` 저장소 루트의 `AGENTS.md` 규칙을 먼저 확인한 뒤**, 그 작업 트리에서 바로 수정합니다.
현재 운영 기준에서는 `Codex Live`, 일반 채팅, 작업메모 반영 요청 모두 **현재 프로젝트 루트의 로컬 `main` 작업본을 바로 수정**합니다. 별도 브랜치 생성이나 `release -> main` 동기화는 기본 전제로 사용하지 않으며, Git 관련 작업은 사용자가 명시적으로 요청할 때만 수행합니다.
현재 운영 기준에서는 `Codex Live`, 일반 채팅, 일반 작업메모 반영 요청 **현재 프로젝트 루트의 로컬 `main` 작업본을 바로 수정**합니다.
단, 자동화 작업메모(`auto_worker`)는 예외적으로 아래 Git 흐름을 기본 동작으로 사용합니다.
- 신규 `feature/*` 브랜치 생성
- 자동 작업 수행
- `release` 브랜치 반영
- `main` 일괄반영
- `PLAN_MAIN_PROJECT_REPO_PATH` 기준 프로젝트 루트에서 `pull --ff-only`
브라우저 기준 접속 확인, 화면 검증, 외부 도메인 테스트는 **`https://test.sm-home.cloud/`를 기본 작업 도메인으로 사용**합니다. 별도 요청이 없는 한 `sm-home.cloud``rel.sm-home.cloud`는 기본 검증 대상으로 삼지 않습니다.
@@ -69,14 +77,15 @@ npm run server-command:runner
`Plan` 게시판 항목을 작업 큐처럼 읽어 자동화할 수 있습니다.
현재 로컬 운영 모드에서는 아래 자동 브랜치 흐름을 기본 동작으로 강제하지 않습니다. 필요 시 사용자가 별도로 요청한 경우에만 사용합니다.
현재 운영 기준에서 자동화 작업메모는 아래 브랜치 흐름을 기본 동작으로 사용합니다.
- `등록` 상태: worker가 읽어서 `feature/plan-{id}-{workId}` 브랜치 생성 시도
- `등록` 상태: worker가 읽어서 신규 `feature/plan-{id}-{workId}` 브랜치 생성
- 성공 시: `작업중`, `브랜치준비`
- 실패 시: `이슈`, 최근 오류 기록
- `개발완료` 상태: worker가 `release` 브랜치 병합 시도
- 병합 성공 시: `완료`
- 병합 실패 시: `이슈`
- `main` 반영 성공 시: `PLAN_MAIN_PROJECT_REPO_PATH`에서 `main` 브랜치 `pull --ff-only`까지 수행
안전 조건:

View File

@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { buildPlanNotificationData } from './plan-notification-service.js';
import { shouldNotifyPlanRestart } from './plan-notification-policy.js';
import { shouldTriggerRetryFromActionNote } from './plan-retry-policy.js';
import { issueActionSchema } from './plan-service.js';
import { issueActionSchema, shouldUseLocalMainPlanMode } from './plan-service.js';
test('shouldTriggerRetryFromActionNote detects missing-fix and verification follow-up requests', () => {
assert.equal(shouldTriggerRetryFromActionNote('누락된 거 다시 고쳐서 테스트해 줘'), true);
@@ -52,3 +52,9 @@ test('buildPlanNotificationData uses stable task key per plan', () => {
notificationKey: 'plan:17',
});
});
test('shouldUseLocalMainPlanMode keeps auto_worker on branch workflow', () => {
process.env.PLAN_LOCAL_MAIN_MODE = 'true';
assert.equal(shouldUseLocalMainPlanMode('auto_worker'), false);
assert.equal(shouldUseLocalMainPlanMode('none'), true);
});

View File

@@ -268,6 +268,11 @@ export function buildPlanBranchName(workId: string, id: number) {
return `${prefix}/plan-${id}-${token}`;
}
export function shouldUseLocalMainPlanMode(automationType: unknown) {
const env = getEnv();
return Boolean(env.PLAN_LOCAL_MAIN_MODE) && normalizePlanAutomationType(automationType) !== 'auto_worker';
}
export function mapPlanRow(
row: Record<string, unknown>,
options?: PlanRowOptions,
@@ -2399,7 +2404,6 @@ export async function listPlanIssueSummaries(planItemIds: number[]) {
export async function claimNextPlanForBranch(workerId: string) {
await ensurePlanTable();
const env = getEnv();
return db.transaction(async (trx) => {
const row = await trx(PLAN_TABLE)
@@ -2417,7 +2421,9 @@ export async function claimNextPlanForBranch(workerId: string) {
return null;
}
const assignedBranch = env.PLAN_LOCAL_MAIN_MODE ? env.PLAN_MAIN_BRANCH : buildPlanBranchName(String(row.work_id), Number(row.id));
const assignedBranch = shouldUseLocalMainPlanMode(row.automation_type)
? String(getEnv().PLAN_MAIN_BRANCH)
: buildPlanBranchName(String(row.work_id), Number(row.id));
const rows = await trx(PLAN_TABLE)
.where({ id: row.id })
.update({

View File

@@ -30,6 +30,7 @@ import {
markPlanReleaseMerged,
markPlanMerged,
markPlanWorkCompleted,
shouldUseLocalMainPlanMode,
upsertAutoPlanItem,
} from '../services/plan-service.js';
import {
@@ -200,6 +201,10 @@ export class PlanWorker {
return Boolean(getEnv().PLAN_LOCAL_MAIN_MODE);
}
private shouldUseLocalMainModeForPlan(item: { automationType?: unknown }) {
return shouldUseLocalMainPlanMode(item.automationType);
}
start() {
const env = getEnv();
@@ -613,8 +618,10 @@ export class PlanWorker {
planId: number,
workId: string,
note: unknown,
automationType: unknown,
) {
const env = getEnv();
const useLocalMainMode = shouldUseLocalMainPlanMode(automationType);
const runCodexCommandAttempt = async (attempt: number) =>
await new Promise<string>((resolve, reject) => {
let settled = false;
@@ -629,7 +636,7 @@ export class PlanWorker {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
PLAN_REPO_PATH: env.PLAN_LOCAL_MAIN_MODE ? env.PLAN_MAIN_PROJECT_REPO_PATH : env.PLAN_GIT_REPO_PATH,
PLAN_REPO_PATH: useLocalMainMode ? env.PLAN_MAIN_PROJECT_REPO_PATH : env.PLAN_GIT_REPO_PATH,
PLAN_API_BASE_URL: 'http://127.0.0.1:3100/api',
PLAN_ACCESS_TOKEN: ERROR_LOG_VIEW_TOKEN,
PLAN_ITEM_ID: String(planId),
@@ -637,7 +644,7 @@ export class PlanWorker {
PLAN_CODEX_TEMPLATE_HOME: env.PLAN_CODEX_TEMPLATE_HOME,
PLAN_GIT_USER_NAME: env.PLAN_GIT_USER_NAME,
PLAN_GIT_USER_EMAIL: env.PLAN_GIT_USER_EMAIL,
PLAN_LOCAL_MAIN_MODE: env.PLAN_LOCAL_MAIN_MODE ? 'true' : 'false',
PLAN_LOCAL_MAIN_MODE: useLocalMainMode ? 'true' : 'false',
PLAN_SKIP_WORK_COMPLETE: 'true',
},
});
@@ -907,7 +914,7 @@ export class PlanWorker {
const releaseTarget = String(item.releaseTarget ?? env.PLAN_RELEASE_BRANCH);
try {
if (!this.isLocalMainMode()) {
if (!this.shouldUseLocalMainModeForPlan(item)) {
await cleanAutomationWorktree(env.PLAN_GIT_REPO_PATH);
await ensureBranchExists(
{
@@ -926,8 +933,8 @@ export class PlanWorker {
return;
}
this.logger.info(
{ planId, branch: assignedBranch, localMainMode: this.isLocalMainMode() },
this.isLocalMainMode() ? 'Plan local main execution prepared' : 'Plan branch created',
{ planId, branch: assignedBranch, localMainMode: this.shouldUseLocalMainModeForPlan(item) },
this.shouldUseLocalMainModeForPlan(item) ? 'Plan local main execution prepared' : 'Plan branch created',
);
} catch (error) {
const message = error instanceof Error ? error.message : '브랜치 생성에 실패했습니다.';
@@ -975,7 +982,7 @@ export class PlanWorker {
const autoDeployToMain = Boolean(item.autoDeployToMain ?? true);
try {
if (this.isLocalMainMode()) {
if (this.shouldUseLocalMainModeForPlan(item)) {
const completedRow = await markPlanAsCompleted(
planId,
'로컬 main 직접 작업 모드에서 release/main 반영 단계를 건너뛰고 완료 처리했습니다.',
@@ -1071,7 +1078,7 @@ export class PlanWorker {
const planLabel = formatPlanNotificationLabel(workId, planId);
try {
if (this.isLocalMainMode()) {
if (this.shouldUseLocalMainModeForPlan(item)) {
const completedRow = await markPlanAsCompleted(
planId,
'로컬 main 직접 작업 모드에서 main 반영 단계를 건너뛰고 완료 처리했습니다.',
@@ -1201,7 +1208,7 @@ export class PlanWorker {
'work-started',
);
const output = await this.runCodexCommandWithProgressNotifications(planId, workId, item.note);
const output = await this.runCodexCommandWithProgressNotifications(planId, workId, item.note, item.automationType);
if (output.includes('처리할 Plan 항목이 없습니다.')) {
throw new Error('자동 작업 대상 Plan 항목을 찾지 못했습니다. 상태 전환 로직을 확인해 주세요.');
@@ -1229,7 +1236,7 @@ export class PlanWorker {
return;
}
const finalCompletedRow = this.isLocalMainMode()
const finalCompletedRow = this.shouldUseLocalMainModeForPlan(item)
? await markPlanAsCompleted(planId, '로컬 main 직접 작업으로 자동 작업을 완료했습니다.')
: await markPlanWorkCompleted(planId, this.workerId, '자동 작업을 완료했습니다.');
if (!finalCompletedRow) {
@@ -1240,7 +1247,9 @@ export class PlanWorker {
planId,
workId,
planLabel,
this.isLocalMainMode() ? '자동 작업이 로컬 main 작업본에 직접 반영되어 완료되었습니다.' : this.buildExecutionCompletedBody(autoDeployToMain),
this.shouldUseLocalMainModeForPlan(item)
? '자동 작업이 로컬 main 작업본에 직접 반영되어 완료되었습니다.'
: this.buildExecutionCompletedBody(autoDeployToMain),
'work-completed',
);
this.logger.info({ planId }, 'Plan Codex execution completed');