feat: v2.2.3 - Stability, Self-Reflector & Intent Alignment

- 버전 2.2.3 상향 및 PATCHNOTES.md 업데이트

- [신규] src/features/selfReflector/ - 성찰 실행/검증/프롬프트 모듈 추가

- [신규] intentAlignment.ts, intentClassifier.ts - 의도 정렬 시스템 추가

- [신규] pixelOfficeState.ts - 픽셀 오피스 상태 관리 추가

- sidebarProvider, dispatcher, chatHandlers 핵심 로직 최적화

- astra-2.2.3.vsix 패키지 생성 완료 (298 tests PASS)
This commit is contained in:
2026-05-15 14:16:14 +09:00
parent ed7e497194
commit 72412450c3
33 changed files with 4964 additions and 125 deletions
+280
View File
@@ -0,0 +1,280 @@
/**
* Pixel Office — Agent Work Pipeline 상태를 시각화하는 *UI Layer 전용* 모듈.
*
* ─────────────────── 설계 원칙 ───────────────────
* 1. **Agent 핵심 판단 로직을 절대 바꾸지 않는다.** Pipeline 진행, contract
* 합의, 검수 cycle, 승인 게이트 — 모두 기존 dispatcher / chatHandlers /
* SidebarChatProvider 안에서 결정된다. 이 모듈은 그 결정을 *읽고* webview용
* 상태 객체로 변환할 뿐이다.
* 2. 입력은 기존에 emit되던 이벤트 (CompanyTurnEvent + alignment phase +
* intent classifier 결과)뿐. 새로운 인터럽트 포인트를 만들지 않는다.
* 3. 출력은 두 종류 — `AgentWorkState` (현재 작업 패널)와 `AgentBubble` 큐
* (말풍선 연출). 둘 다 webview가 그대로 받아 그리기만 하면 된다.
*
* 즉 dispatcher 안의 어떤 한 줄도 "if pixelOffice ..."로 분기하지 않는다.
*/
/** Agent의 현재 단계. 사용자가 명세한 11개 상태값 전부 포함. */
export type AgentStatus =
| 'idle'
| 'intake'
| 'analyzing'
| 'need_clarification'
| 'contract_ready'
| 'planning'
| 'executing'
| 'reviewing'
| 'waiting_approval'
| 'error'
| 'done';
/** 말풍선이 어떤 카테고리에서 나왔는지 — 스타일링 / 우선순위에 활용. */
export type BubbleType = 'status' | 'event' | 'warning' | 'error' | 'success';
/** 말풍선 발생 트리거가 되는 이벤트 — 사용자가 명세한 10개 + 약간 확장. */
export type AgentEvent =
| 'missing_required_info'
| 'clarification_needed'
| 'requirement_contract_created'
| 'plan_completed'
| 'execution_started'
| 'review_failed'
| 'review_passed'
| 'risky_change_detected'
| 'approval_required'
| 'error_occurred'
| 'task_completed'
| 'stage_loop_retry';
export interface AgentWorkState {
agentId: string;
agentName: string;
status: AgentStatus;
/** 사용자 원본 요청 한 줄 — 패널 상단 "Current Task"에 표시. */
currentTask?: string;
/** 현재 stage / phase 라벨 — "기획 논의", "QA 진행" 등. */
currentStep?: string;
/** 다음 stage 라벨 (있으면) — 예측 표시용. */
nextStep?: string;
/** 짧은 보조 메시지 (예: "라운드 2/3", "검수자: 민지"). */
message?: string;
/** 진행률 0~1 — 파이프라인 모드일 때 stage index / total로 계산. */
progress?: number;
/** Requirement Contract 요약 (alignment 완료 후 채워짐). */
requirementContract?: {
goal?: string;
context?: string;
criteria?: string[];
format?: string;
openQuestions?: string[];
confidence?: 'low' | 'medium' | 'high';
};
/** 사용자에게 던지는 미해결 질문 목록 — need_clarification 상태에서 채움. */
needUserInput?: string[];
/** 승인 대기 중 항목 — waiting_approval 상태에서 채움. */
awaitingApproval?: string;
/** 짧은 최근 로그 — 사용자가 한눈에 흐름 파악. 최대 6개 ring buffer. */
recentLogs?: string[];
/** epoch ms — webview의 "n초 전" 표시용. */
updatedAt: number;
}
export interface AgentBubble {
id: string;
/** 어떤 캐릭터 위에 띄울지 — 단일 캐릭터 모드면 'main' 고정도 가능. */
agentId: string;
text: string;
type: BubbleType;
/** 생성 시각 epoch ms. */
createdAt: number;
/** 자동 사라짐 ms (webview가 사용). 기본값은 webview에서 결정. */
durationMs?: number;
}
/**
* 사용자가 설정으로 켜고 끌 수 있는 행동 옵션.
* webview는 broadcast마다 같이 받아서 즉시 반영.
*/
export interface PixelOfficeConfig {
enabled: boolean;
bubblesEnabled: boolean;
maxVisibleBubbles: number;
bubbleDurationMs: number;
}
// ─────────────────── 상태→말풍선 텍스트 풀 ───────────────────
// 사용자가 명세한 톤(가벼운 사무실 코미디) 유지. 무작위 선택을 위해 같은 상태에
// 여러 안을 두되 너무 길어지지 않게 4~5개로 제한.
const STATUS_BUBBLE_POOL: Record<AgentStatus, string[]> = {
idle: [
'오늘은 무슨 일을 할까?',
'주문 대기 중…',
'커피 한 잔 더 하고 시작할까.',
],
intake: [
'요청서 들어왔다.',
'한번 읽어보자.',
'오케이, 뭘 원하시는지 보자.',
],
analyzing: [
'음… 의도가 조금 모호한데?',
'맥락부터 정리해보자.',
'핵심이 뭐였더라.',
'이거 작업 범위가 어디까지지?',
],
need_clarification: [
'이건 사용자 확인이 먼저야.',
'질문 하나만 하고 가자.',
'추측으로 가면 위험해.',
'핵심 정보가 빠졌네.',
],
contract_ready: [
'좋아, 작업 조건 정리 완료.',
'이제 방향은 잡혔어.',
'계약서 도장 찍었다.',
'이제 진짜 시작.',
],
planning: [
'순서부터 잡아보자.',
'기존 기능은 건드리지 말자.',
'화이트보드 좀 빌릴게.',
'단계 나눠서 가자.',
],
executing: [
'코드 들어간다.',
'이번엔 단순하게 가자.',
'집중 모드 진입.',
'키보드 워밍업 완료.',
],
reviewing: [
'잠깐, 이건 다시 보자.',
'기존 기능 깨지는지 확인해야 해.',
'검수자 시점으로 한 번 더.',
'엣지 케이스 빠진 거 없나.',
],
waiting_approval: [
'이건 승인 없이 못 바꿔.',
'위험 작업 감지. 확인 필요!',
'사장님 결재 부탁드립니다.',
'도장 받기 전엔 멈춤.',
],
error: [
'앗, 이건 예상 못 했는데…',
'조건 하나 놓쳤네.',
'잠깐, 다시 정리.',
'엇, 이게 깨졌네.',
],
done: [
'좋아, 끝났다!',
'이번 작업 깔끔하게 완료.',
'커피 한 잔.',
'오늘치 끝!',
],
};
const EVENT_BUBBLE_POOL: Record<AgentEvent, string[]> = {
missing_required_info: [
'핵심 정보가 빠졌어.',
'이거 빠지면 추측해야 해.',
],
clarification_needed: [
'질문 하나만.',
'확실하지 않으면 먼저 물어보자.',
],
requirement_contract_created: [
'계약서 도장 찍었다.',
'이제 방향은 잡혔어.',
],
plan_completed: [
'계획 정리 끝!',
'순서대로 가자.',
],
execution_started: [
'코드 들어간다.',
'시작!',
],
review_failed: [
'조건 하나 놓쳤네. 다시 보자.',
'너무 복잡하게 가는 거 아냐?',
],
review_passed: [
'검수 통과!',
'셋 다 만족이래.',
],
risky_change_detected: [
'잠깐, 이건 승인 필요!',
'파일 삭제는 함부로 하면 안 돼.',
],
approval_required: [
'결재 부탁드립니다.',
'도장 받고 이어 갈게.',
],
error_occurred: [
'앗, 이건 예상 못 했는데…',
'엇, 이게 깨졌네.',
],
task_completed: [
'좋아, 끝났다!',
'오늘치 끝.',
],
stage_loop_retry: [
'한 번 더 가자.',
'버그 잡고 다시 시도.',
],
};
/**
* 텍스트 풀에서 하나 뽑기. 같은 결과가 연달아 나오지 않게 lastPicked를
* 받아 회피. 풀이 1개뿐이면 어쩔 수 없이 그걸 반환.
*/
export function pickBubbleText(pool: string[], lastPicked?: string): string {
if (!pool || pool.length === 0) return '';
if (pool.length === 1) return pool[0];
let candidates = pool;
if (lastPicked) candidates = pool.filter((s) => s !== lastPicked);
if (candidates.length === 0) candidates = pool;
return candidates[Math.floor(Math.random() * candidates.length)];
}
export function getStatusBubbleText(status: AgentStatus, lastPicked?: string): string {
return pickBubbleText(STATUS_BUBBLE_POOL[status] ?? [], lastPicked);
}
export function getEventBubbleText(event: AgentEvent, lastPicked?: string): string {
return pickBubbleText(EVENT_BUBBLE_POOL[event] ?? [], lastPicked);
}
/** 이벤트 → 말풍선 type 매핑. 색상/스타일을 webview가 결정할 때 사용. */
export function eventBubbleType(event: AgentEvent): BubbleType {
switch (event) {
case 'error_occurred': return 'error';
case 'review_failed':
case 'risky_change_detected':
case 'missing_required_info':
return 'warning';
case 'task_completed':
case 'review_passed':
case 'plan_completed':
case 'requirement_contract_created':
return 'success';
default: return 'event';
}
}
/** AgentBubble factory — id 자동 생성. */
export function makeBubble(opts: {
agentId: string;
text: string;
type?: BubbleType;
durationMs?: number;
}): AgentBubble {
return {
id: `b-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
agentId: opts.agentId,
text: opts.text,
type: opts.type ?? 'status',
createdAt: Date.now(),
durationMs: opts.durationMs,
};
}