feat(growth): 주간 성장 사이클 자동화 + 텔레그램 양방향 HITL (v2.2.220)

P4 — Self-Evolving OS 폐루프 자동화:
- growthCycleWatcher: 매주(기본 일 20:00 KST, 설정 가능) 자동으로
  ① 골든셋 검색 평가(recall/MRR 주간 추이) ② 학습 큐 갱신(Need Engine)
  ③ 지식 노후 점검 ④ 성장 리포트 ⑤ 승인(approved)된 학습 큐 항목을
  Research Agent 로 자동 실행(사이클당 최대 3건) ⑥ 요약 알림+텔레그램.
  승인 자체는 여전히 사람 — Permission Based Learning 유지, 자동화되는
  것은 '승인된 것의 실행'뿐. 결과물은 기존 수동 명령과 동일 위치
  (.astra/eval/, .astra/growth/) — 완전 호환. 수동 트리거 명령
  (growthCycle.runNow) 제공. 단계별 독립 try/catch.

P5 — 텔레그램 양방향 HITL:
- /meet confirm 코어를 출력 중립 processConfirmDecisions 로 추출
  (웹뷰·텔레그램 공용) — 핸들러는 위임 호출로 슬림화.
- 텔레그램 인바운드에 confirm/pending(보류) 분기 — 회사 밖에서
  "confirm 1=ok 2=6/20 3=skip" 회신으로 보류 액션 등록 완결.
- 데일리 브리핑에 보류 목록 + 회신 안내 포함 — 아침 브리핑에서
  바로 확정하는 흐름 완성.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 18:29:58 +09:00
parent b540923890
commit b72501fae5
8 changed files with 423 additions and 105 deletions
@@ -252,6 +252,110 @@ export function logMeetRegistration(event: string, data: Record<string, unknown>
logInfo(`/meet 등록 게이트: ${event}`, data);
}
// ── confirm 처리 코어 (출력 중립 — 웹뷰·텔레그램 공용) ──────────────────────
// runMeetConfirm(웹뷰)과 텔레그램 인바운드가 같은 로직을 쓰도록 문자열 라인을
// 반환한다. 원격(텔레그램)에서도 보류 답변→등록 완결이 가능해진다 (P5 HITL).
import { readCalendarConfig } from '../../calendar/calendarCache';
export async function processConfirmDecisions(
context: vscode.ExtensionContext,
arg: string,
): Promise<{ lines: string[]; remaining: number }> {
const lines: string[] = [];
const pend = loadPending();
if (!pend || !pend.items.length) {
return { lines: ['ℹ️ 등록 보류 중인 액션 아이템이 없습니다.'], remaining: 0 };
}
if (!arg.trim()) {
return { lines: [renderPendingQuestion(pend)], remaining: pend.items.length };
}
const calCfg = readCalendarConfig(context);
if (!calCfg.refreshToken) {
return { lines: ['⚠️ Google OAuth(쓰기)가 연결되지 않아 등록할 수 없습니다. (Astra Settings → Google 섹션)'], remaining: pend.items.length };
}
const gCfg = vscode.workspace.getConfiguration('g1nation');
const useTasks = gCfg.get<boolean>('meetUsesTasks', true);
const useCalendar = gCfg.get<boolean>('meetUsesCalendar', true);
const { decisions, errors } = parseConfirmArgs(arg, new Date().getFullYear());
for (const e of errors) lines.push(`⚠️ ${e}`);
if (!decisions.length) {
lines.push('해석 가능한 답변이 없습니다. 예: confirm 1=6/20 2=ok 3=skip');
return { lines, remaining: pend.items.length };
}
let registered = 0, skipped = 0, failed = 0;
const doneIdx = new Set<number>();
const newKeys: string[] = [];
for (const d of decisions) {
const item = pend.items.find(i => i.idx === d.idx);
if (!item) { lines.push(`⚠️ ${d.idx}번 항목이 보류 목록에 없습니다.`); continue; }
if (d.action === 'skip') {
skipped++; doneIdx.add(item.idx);
lines.push(`⏭️ ${item.idx}. ${item.work} — 등록 안 함`);
continue;
}
const isConditional = item.kind === 'conditional';
let title: string;
let date: string | undefined;
const extra: string[] = [];
if (isConditional) {
extra.push(`■ 선행 조건: ${item.condition}`);
if (d.action === 'ok') {
title = `[조건부] ${item.work}`;
date = undefined;
extra.push('· 선행 조건 충족 후 진행 — 날짜 없는 task 로 등록됨 (충족 시 날짜 부여)');
if (!useTasks) {
failed++;
lines.push(`⚠️ ${item.idx}. ${item.work} — 날짜 없는 등록은 Tasks 가 필요합니다. 확인일을 지정하세요: ${item.idx}=날짜`);
continue;
}
} else {
title = `[조건부 확인] ${item.work}`;
date = d.date!;
extra.push(`· ${date} 는 선행 조건 충족 여부를 점검하는 확인일입니다.`);
}
} else {
title = item.work;
date = d.action === 'ok' ? item.suggestedDate : d.date!;
}
const notes = buildNotes({
detail: item.detail, meetTitle: pend.meetTitle, owner: item.owner,
dueRaw: item.due, dateLabel: date || '(날짜 없음 — 조건부)', extra,
});
const r = await registerAction(context, {
title, date, notes,
useTasks, useCalendar: isConditional && !date ? false : useCalendar,
});
if (r.failures.length === 0) {
registered++; doneIdx.add(item.idx); newKeys.push(taskKey(item.work));
lines.push(`${date || '(날짜 없음)'}${title} (${r.successes.join(' + ')})`);
} else {
failed++;
if (r.successes.length) { doneIdx.add(item.idx); newKeys.push(taskKey(item.work)); }
lines.push(`${date || '(날짜 없음)'}${title}${r.successes.length ? ` (✅ ${r.successes.join(' + ')})` : ''}`);
for (const f of r.failures) lines.push(` ⚠️ ${f}`);
}
}
if (newKeys.length) markRegistered(pend.transcriptHash, pend.meetTitle, newKeys);
const remaining = pend.items.filter(i => !doneIdx.has(i.idx));
if (remaining.length === 0) {
clearPending();
lines.push(`✅ 보류 항목 처리 완료 — 등록 ${registered}건 · 건너뜀 ${skipped}${failed ? ` · 실패 ${failed}` : ''}`);
} else {
savePending({ ...pend, items: remaining });
lines.push(`등록 ${registered}건 · 건너뜀 ${skipped}${failed ? ` · 실패 ${failed}` : ''} — 아직 ${remaining.length}건 보류 중 (pending 으로 확인)`);
}
return { lines, remaining: remaining.length };
}
// ── 회의 용어집 (반복 회의의 STT 보정 정확도용) ─────────────────────────────
// meetPrompt 는 메타데이터를 "용어집 역할"로 쓴다 — 매번 수동 입력하는 대신,
// 이전 /meet 실행에서 나온 인명(담당)·사용자 입력 메타데이터 용어를 워크스페이스에