feat(weekly): /weekly 주간 보고서(금주/차주) 추가 + /meet 정확도 개선 (v2.2.204)

- /weekly: 차주 날짜 입력→금주 자동 역산, Google Tasks 기반 금주/차주 보고서.
  버킷팅은 코드(예측 가능), 포맷팅만 LLM. 신규 weeklyPrompt.ts + coordination.ts runWeekly.
- 기존 CEO /weekly 리뷰 카드(dashboards.ts) 제거 — 이름 충돌 해소, /weekly 일원화.
- /meet: 액션아이템에 '작업 상세' 열 추가, 캘린더 notes 가 실제 작업 내용을 담도록 재구성.
- /meet: 발언자 추적 복원 + 비선형 회의 재조립 + 근거/할루시네이션 억제 규칙으로 오귀속 감소.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 16:12:33 +09:00
parent ebfce17b03
commit 2ea5185cd6
8 changed files with 257 additions and 335 deletions
@@ -15,6 +15,8 @@ import { registerSlashCommand, chunk } from '../../datacollect/slashRouter';
import { parseTaskOwner } from './_shared';
import { createCalendarEvent, createTask, listTasks, _addDaysDate } from '../../calendar';
import { ChronicleProjectStore } from '../../../sidebar/managers/chronicleProjectStore';
import { callLmSynthesis } from '../../datacollect/llm';
import { buildWeeklyPrompt, WEEKLY_SYSTEM, type WeeklyTask } from '../../datacollect/prompts/weeklyPrompt';
// ─── 공통 헬퍼 — /task 전용 ──────────────────────────────────────────────
@@ -563,9 +565,100 @@ async function runStandup(arg: string, view: any, context?: vscode.ExtensionCont
return true;
}
// ─── /weekly — 캘린더 task 기반 주간 보고서 (금주/차주) ──────────────────
// `/weekly <차주시작일> <차주종료일>` — 입력한 날짜는 **차주** 기준.
// 금주(차주 시작 직전 7일)는 자동 역산해 함께 검색한다. 버킷팅은 due/completed
// 날짜로 코드가 처리(예측 가능), 서술 포맷팅만 LLM(meet 메모 → narrative bullet).
async function runWeekly(arg: string, view: any, context?: vscode.ExtensionContext): Promise<boolean> {
if (!context) { chunk(view, '\n❌ ExtensionContext 없음 — /weekly 실행 불가.\n'); return true; }
const tokens = arg.trim().split(/\s+/).filter(Boolean);
if (tokens.length < 2) {
chunk(view, [
'\n📋 **/weekly — 주간 업무 보고서 (금주/차주)**',
'',
'사용법: `/weekly <차주 시작일> <차주 종료일>`',
' · 입력한 두 날짜는 **차주** 기준입니다.',
' · 금주(차주 시작 직전 7일)는 자동으로 역산해 함께 검색합니다.',
'',
'날짜 형식: `YYYY-MM-DD` · `YYYY/MM/DD` · `YY/MM/DD`',
'',
'예시: `/weekly 2026-06-08 2026-06-12` → 차주 6/8~6/12, 금주 6/1~6/7',
'',
'캘린더(Google Tasks)에 등록된 작업의 마감·완료일로 금주/차주를 나누고,',
'각 작업의 메모(작업 상세·맥락)를 근거로 하위 세부 항목을 채웁니다.',
'※ `/meet`·`/task` 로 등록한 작업이 소스입니다 — 메모가 충실할수록 보고서가 정확합니다.',
'',
].join('\n'));
return true;
}
let nextStart = parseFlexibleDate(tokens[0]);
let nextEnd = parseFlexibleDate(tokens[1]);
if (!nextStart || !nextEnd) {
chunk(view, `\n❌ 날짜 형식 오류 — "${tokens[0]}" / "${tokens[1]}". 사용 가능: YYYY-MM-DD · YYYY/MM/DD · YY/MM/DD.\n`);
return true;
}
if (nextStart > nextEnd) { [nextStart, nextEnd] = [nextEnd, nextStart]; }
// 금주 = 차주 시작 직전 7일 (차주 시작 -7 ~ 차주 시작 -1).
const thisStart = _addDaysDate(nextStart, -7);
const thisEnd = _addDaysDate(nextStart, -1);
chunk(view, `\n📊 **주간 보고서**\n · 금주: ${thisStart} ~ ${thisEnd}\n · 차주: ${nextStart} ~ ${nextEnd}\n`);
chunk(view, '\n📥 Tasks 가져오는 중...\n');
const result = await listTasks(context, { showCompleted: true, maxResults: 300 });
if (!result.ok) { chunk(view, `\n❌ Tasks 조회 실패: ${result.error}\n`); return true; }
const inRange = (d: string, a: string, b: string) => !!d && d >= a && d <= b;
const toWeekly = (t: typeof result.tasks[number]): WeeklyTask => ({
title: t.title,
due: t.due || '',
status: t.status,
completedYmd: t.completed ? t.completed.slice(0, 10) : undefined,
notes: t.notes,
});
const thisWeek: WeeklyTask[] = [];
const nextWeek: WeeklyTask[] = [];
for (const t of result.tasks) {
const completedYmd = (t.completed || '').slice(0, 10);
if (inRange(t.due || '', nextStart, nextEnd)) {
nextWeek.push(toWeekly(t));
} else if (inRange(t.due || '', thisStart, thisEnd) || inRange(completedYmd, thisStart, thisEnd)) {
thisWeek.push(toWeekly(t));
}
}
if (thisWeek.length === 0 && nextWeek.length === 0) {
chunk(view, `\n️ 금주(${thisStart}~${thisEnd})·차주(${nextStart}~${nextEnd}) 범위에 등록된 task 가 없습니다.\n (\`/meet\` 또는 \`/task\` 로 등록하면 잡힙니다.)\n`);
return true;
}
chunk(view, `\n· 금주 ${thisWeek.length}건 · 차주 ${nextWeek.length}건 → 보고서 합성 중…\n`);
let report = '';
try {
report = await callLmSynthesis(
buildWeeklyPrompt({ thisWeek, nextWeek, thisRange: [thisStart, thisEnd], nextRange: [nextStart, nextEnd] }),
WEEKLY_SYSTEM,
);
if (!report) throw new Error('LLM 응답이 비어 있습니다.');
} catch (e: any) {
chunk(view, `\n⚠️ 보고서 합성 실패: ${e?.message || String(e)}\n(LM 서버가 떠 있는지, \`g1nation.ollamaUrl\` / \`defaultModel\` 설정을 확인하세요.)\n`);
return true;
}
chunk(view, '\n' + report.trim() + '\n');
return true;
}
// ─── 등록 ─────────────────────────────────────────────────────────────────
registerSlashCommand({ name: '/task', description: '단일 작업을 Google Tasks + Calendar 양쪽에 등록 (제목 + 시작일 + 완료일)', handler: runTask });
registerSlashCommand({ name: '/weekly', description: '캘린더 task 기반 주간 보고서 — 차주 날짜 입력, 금주 자동 역산 (금주/차주 포맷)', handler: runWeekly });
registerSlashCommand({ name: '/decisions', description: 'Chronicle 결정 기록(ADR) 검색 — 키워드/담당자로 필터', handler: runDecisions });
registerSlashCommand({ name: '/onesie', description: '멤버별 1:1 미팅 준비 카드 — Tasks 진행 + 최근 결정 + 대화 토픽 제안', handler: runOnesie });
registerSlashCommand({ name: '/blocked', description: '전사 across 지연·블로커 한 화면 — 일일 아침 ritual', handler: runBlocked });