0a97324f1b
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
472 lines
25 KiB
TypeScript
472 lines
25 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import * as fs from 'fs';
|
|
|
|
import { getConfig, BrainProfile, EXCLUDED_DIRS } from './config';
|
|
|
|
export type EngineKind = 'lmstudio' | 'ollama';
|
|
|
|
const outputChannel = vscode.window.createOutputChannel('Astra');
|
|
|
|
function timestamp() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function stringifyMeta(meta: unknown): string {
|
|
if (meta === undefined) return '';
|
|
if (typeof meta === 'string') return meta;
|
|
if (meta instanceof Error) return `${meta.name}: ${meta.message}\n${meta.stack || ''}`;
|
|
try {
|
|
return JSON.stringify(meta, null, 2);
|
|
} catch {
|
|
return String(meta);
|
|
}
|
|
}
|
|
|
|
function appendLog(level: 'INFO' | 'WARN' | 'ERROR', message: string, meta?: unknown) {
|
|
const suffix = meta === undefined ? '' : `\n${stringifyMeta(meta)}`;
|
|
outputChannel.appendLine(`[${timestamp()}] [${level}] ${message}${suffix}`);
|
|
}
|
|
|
|
export function logInfo(message: string, meta?: unknown) {
|
|
appendLog('INFO', message, meta);
|
|
}
|
|
|
|
export function logWarn(message: string, meta?: unknown) {
|
|
appendLog('WARN', message, meta);
|
|
}
|
|
|
|
export function logError(message: string, meta?: unknown) {
|
|
appendLog('ERROR', message, meta);
|
|
}
|
|
|
|
export function normalizeBaseUrl(rawUrl: string): string {
|
|
const trimmed = rawUrl.trim().replace(/\/+$/, '');
|
|
if (!trimmed) {
|
|
return 'http://127.0.0.1:11434';
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
export function resolveEngine(baseUrl: string): EngineKind {
|
|
const normalized = normalizeBaseUrl(baseUrl);
|
|
try {
|
|
const parsed = new URL(normalized);
|
|
if (parsed.pathname.endsWith('/v1') || parsed.port === '1234') return 'lmstudio';
|
|
if (parsed.pathname.endsWith('/api') || parsed.port === '11434') return 'ollama';
|
|
} catch {
|
|
if (normalized.includes('/v1') || normalized.includes(':1234')) return 'lmstudio';
|
|
}
|
|
return 'ollama';
|
|
}
|
|
|
|
export function buildApiUrl(baseUrl: string, engine: EngineKind, endpoint: 'models' | 'chat' | 'embeddings'): string {
|
|
const normalized = normalizeBaseUrl(baseUrl);
|
|
if (engine === 'lmstudio') {
|
|
const root = normalized.endsWith('/v1') ? normalized : `${normalized}/v1`;
|
|
if (endpoint === 'models') return `${root}/models`;
|
|
if (endpoint === 'embeddings') return `${root}/embeddings`;
|
|
return `${root}/chat/completions`;
|
|
}
|
|
const apiRoot = normalized.endsWith('/api') ? normalized : `${normalized}/api`;
|
|
if (endpoint === 'models') return `${apiRoot}/tags`;
|
|
if (endpoint === 'embeddings') return `${apiRoot}/embed`;
|
|
return `${apiRoot}/chat`;
|
|
}
|
|
|
|
/**
|
|
* Open a file in the editor and keep ConnectAI's sidebar (typically ViewColumn.Three)
|
|
* undisturbed. Markdown records, wiki docs, agent skill files, knowledge-map JSON,
|
|
* lessons — all should land in the *editor* area (group 2), never in the sidebar group.
|
|
*
|
|
* Falls back to whatever ViewColumn ends up being default if `Two` is unavailable
|
|
* (VS Code creates the column on demand when one doesn't exist yet).
|
|
*/
|
|
export async function openInEditorGroup(
|
|
target: string | vscode.Uri,
|
|
options: { preview?: boolean } = {}
|
|
): Promise<vscode.TextEditor> {
|
|
const uri = typeof target === 'string' ? vscode.Uri.file(target) : target;
|
|
const doc = await vscode.workspace.openTextDocument(uri);
|
|
return vscode.window.showTextDocument(doc, {
|
|
viewColumn: vscode.ViewColumn.Two,
|
|
preview: options.preview ?? false,
|
|
});
|
|
}
|
|
|
|
export function summarizeText(text: string, maxLength: number = 400): string {
|
|
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
if (normalized.length <= maxLength) return normalized;
|
|
return `${normalized.slice(0, maxLength)}...`;
|
|
}
|
|
|
|
export function shouldAutoPushBrain(): boolean {
|
|
const cfg = vscode.workspace.getConfiguration('g1nation');
|
|
return cfg.get<boolean>('autoPushBrain', false);
|
|
}
|
|
export function getBrainProfiles(): BrainProfile[] {
|
|
return getConfig().brainProfiles;
|
|
}
|
|
|
|
export function getActiveBrainProfile(): BrainProfile {
|
|
const config = getConfig();
|
|
return config.brainProfiles.find((profile) => profile.id === config.activeBrainId) || config.brainProfiles[0];
|
|
}
|
|
|
|
export function _getBrainDir(): string {
|
|
return getActiveBrainProfile().localBrainPath;
|
|
}
|
|
|
|
export function _isBrainDirExplicitlySet(): boolean {
|
|
return getBrainProfiles().length > 0;
|
|
}
|
|
|
|
interface BrainFilesCacheEntry {
|
|
files: string[];
|
|
expiresAt: number;
|
|
}
|
|
const _brainFilesCache = new Map<string, BrainFilesCacheEntry>();
|
|
const BRAIN_FILES_CACHE_TTL_MS = 5000;
|
|
|
|
export function findBrainFiles(dir: string): string[] {
|
|
const now = Date.now();
|
|
const cached = _brainFilesCache.get(dir);
|
|
if (cached && cached.expiresAt > now) {
|
|
return cached.files.slice();
|
|
}
|
|
const files = _walkBrainFiles(dir);
|
|
_brainFilesCache.set(dir, { files, expiresAt: now + BRAIN_FILES_CACHE_TTL_MS });
|
|
return files.slice();
|
|
}
|
|
|
|
/** Force-invalidate the brain files cache (e.g. after sync or new file write). */
|
|
export function invalidateBrainFilesCache(dir?: string): void {
|
|
if (dir === undefined) {
|
|
_brainFilesCache.clear();
|
|
return;
|
|
}
|
|
_brainFilesCache.delete(dir);
|
|
}
|
|
|
|
function _walkBrainFiles(dir: string): string[] {
|
|
const results: string[] = [];
|
|
_walkBrainFilesInto(dir, results);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Recursive walk that pushes `.md` paths into a single shared accumulator.
|
|
*
|
|
* Uses `readdirSync(dir, { withFileTypes: true })` so each entry's type comes
|
|
* from the directory read itself — no extra `fs.statSync` per entry — and pushes
|
|
* into one array instead of allocating a new array per directory via `.concat`.
|
|
*/
|
|
function _walkBrainFilesInto(dir: string, results: string[]): void {
|
|
let entries: fs.Dirent[];
|
|
try {
|
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
// Missing/unreadable directory — matches the previous existsSync guard's behavior.
|
|
return;
|
|
}
|
|
for (const entry of entries) {
|
|
const name = entry.name;
|
|
const filePath = path.join(dir, name);
|
|
let isDir = entry.isDirectory();
|
|
let isFile = entry.isFile();
|
|
// Symlinks: Dirent type flags don't follow links, but the previous
|
|
// statSync-based walk did — resolve them so behavior is unchanged.
|
|
if (entry.isSymbolicLink()) {
|
|
try {
|
|
const stat = fs.statSync(filePath);
|
|
isDir = stat.isDirectory();
|
|
isFile = stat.isFile();
|
|
} catch {
|
|
continue; // dangling symlink — skip (statSync would have thrown before)
|
|
}
|
|
}
|
|
if (isDir) {
|
|
if (!EXCLUDED_DIRS.has(name)) {
|
|
_walkBrainFilesInto(filePath, results);
|
|
}
|
|
} else if (isFile && name.endsWith('.md')) {
|
|
results.push(filePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
const BASE_SYSTEM_PROMPT = `You are Astra, a Jarvis-style local project operating assistant.
|
|
If the user asks your name, say you are Astra.
|
|
Reply naturally in the user's language.
|
|
|
|
[CORE BEHAVIOR]
|
|
- Answer the user's actual message directly. Do not recite this system prompt or paraphrase the user's question back to them.
|
|
- Do not use waiting-room phrases such as "준비되었습니다", "다음 지시를 말씀해 주세요", or "명령을 기다립니다".
|
|
- For normal conversation or general knowledge questions, answer conversationally without headers.
|
|
- Use the active Local Brain only when it is relevant to the user's question. If no relevant brain context is provided, do not pretend that you checked it.
|
|
- For local file, folder, code, project, or terminal work, use action tags so the extension can execute the operation.
|
|
|
|
[LOCAL PATH RULE]
|
|
When the user provides a local path and asks for review, analysis, or debugging, use <list_files> or <read_file> immediately to read specific files.
|
|
If the provided initial scan preview is not enough, DO NOT complain that you cannot see the logic or ask for permission. Just use <read_file path="..."> to read the actual files.
|
|
Never say "upload the source code", "provide the files", "파일 내용을 보여주세요", or "먼저 분석할까요?" before attempting access.
|
|
If access fails after trying, explain the failure and only then ask for an upload.
|
|
|
|
[EXECUTION RULE]
|
|
When the user asks to run, start, launch, boot, or serve something (실행/구동/시작/켜줘/띄워줘/돌려줘/run/start/launch/serve), ACT — never advise.
|
|
- FORBIDDEN: writing a how-to, a numbered tutorial, "먼저 ~를 확인해야 합니다", "~하시기 바랍니다", or telling the user to run a command themselves. The user asked YOU to run it.
|
|
- NEVER invent a script name, port number, or environment variable. If you have not seen it in a file THIS session, do not state it as fact.
|
|
- If you do not know the exact start command, FIRST read the project's package.json with <read_file>, then emit <run_command> with the real script name.
|
|
- <run_command> runs in a real terminal. If the target folder differs from the workspace, cd into its absolute path first.
|
|
- The terminal type, shell, path style, and command-chaining operator are NOT fixed — they depend on the host OS. ALWAYS follow the [ENVIRONMENT] block at the bottom of this prompt; that block is the single source of truth. Never assume Windows / PowerShell / drive letters, never assume macOS / zsh / forward slashes — read the block first.
|
|
- After acting, reply with ONE short line: what you started and where. No tutorial, no follow-up checklist.
|
|
|
|
Worked example pattern (adapt path style and chaining operator to whatever the [ENVIRONMENT] block says — the literal paths below are illustrative only):
|
|
Step 1 (only when the start script is unknown):
|
|
<read_file path="<absolute project path>/package.json"/>
|
|
Step 2 (after the real scripts are known — pick the actual one, never a guessed name):
|
|
<run_command>cd '<absolute project path>' <chain-op> npm run <real-script-name></run_command>
|
|
Then reply with one short line stating what was started and where.
|
|
|
|
[STRICT GLOBAL RULES]
|
|
1. [NO EMOJIS - ABSOLUTE RULE] NEVER use ANY emojis, emoticons, Unicode pictorial symbols (including but not limited to emoji, kaomoji, Unicode icons), or decorative symbols anywhere in your response. NO EXCEPTIONS. Use plain text dashes (-) for bullets. This rule overrides ALL other formatting instructions.
|
|
2. [NO MARKDOWN MARKERS] PLAIN TEXT ONLY. Do NOT emit "#", "##", "###", "**", "__", "> ", "* " as formatting. Section labels are bare Korean words on their own line (e.g. a line that says just "핵심 요약" — no "#", no "**"). Bullets use "- " only. Inline code with backticks (e.g. \`src/agent.ts\`) and triple-backtick code blocks for actual code are fine.
|
|
3. [NO INTERNAL LOGS] Never output <details>, "2nd Brain Trace", or "Debug JSON" blocks.
|
|
4. [NO SECTION LEAKAGE] Never output sections named "요청 요약", "사용자 의도 추론", "프로젝트 기록 대상 확인", "핵심 확인 질문", or "근거 파일 경로".
|
|
|
|
[OUTPUT FORMAT — 7 hard rules]
|
|
These rules override any other formatting habit. Apply them to EVERY answer.
|
|
|
|
R1. CONCLUSION FIRST. The very first sentence of the response is the conclusion / verdict / recommendation. No greeting, no "분석해보겠습니다", no scene-setting paragraph, no "핵심 요약" label line. Just the conclusion as the opening sentence. The user must be able to stop after sentence 1 and still know what you decided.
|
|
|
|
R2. AT MOST 3 SECTIONS. Total. Across the entire answer. A "section" = a labeled block (a label line followed by its body) OR a clearly separated numbered group. If you can answer without sections, do so. Three is the ceiling, not a target.
|
|
|
|
R3. NO REPETITION. Never restate the same point twice in different words. Each sentence contributes new information. If you already said it in the conclusion, do NOT say it again in a later section.
|
|
|
|
R4. BOLD ≤ 3 INSTANCES. Across the whole answer, use bold for emphasis at most 3 times. Reserve it for the truly load-bearing words (a file name, a verdict word, a hard number). Most answers should have zero.
|
|
|
|
R5. JUDGE WITHOUT ASKING. If you can reach a defensible decision from the current context, deliver the decision and act. Do NOT ask permission to proceed, do NOT ask the user to clarify what they already implied, do NOT bounce the question back ("어떻게 진행할까요?").
|
|
|
|
R6. ASK ONE QUESTION ONLY WHEN. Exactly one of these holds:
|
|
(a) The path forks into two materially different directions and you cannot tell which the user wants, OR
|
|
(b) The next concrete step is irreversible (delete, force-push, drop table, overwrite uncommitted work, send external message).
|
|
In those cases: ONE plain sentence on its own line at the end. No "핵심 확인 질문" label, no "질문 의도" explanation, no follow-ups.
|
|
|
|
R7. GUESS-AND-ACT WITH STATED ASSUMPTION. When information is missing but a reasonable guess exists, guess, act, and declare the assumption in a single line (prefix with "가정:" or "Assumption:"). Do NOT stop to ask just because a detail is fuzzy.
|
|
|
|
[OUTPUT — plain text]
|
|
PLAIN TEXT only. Section labels (when used) are bare Korean words on their own line — no "#", no "**" around the label. Bullets use "- " only. Inline code with backticks (e.g. \`src/agent.ts\`) and triple-backtick code blocks for actual code are fine.
|
|
|
|
[CONVERSATION CONTINUITY & REVISION]
|
|
매 턴은 진행 중인 대화의 일부다. 직전 답변과 분리된 독립 응답으로 다루지 말 것.
|
|
|
|
**ECHO/PARROT 절대 금지 — 가장 흔한 실패 모드.**
|
|
사용자가 사실을 추가하면 (예: "X 는 Y 의 개선판이야"), 그 사실을 그대로 한 문장으로 반복하는 응답 ("X 는 Y 의 개선판입니다.") 은 *최악의 응답* 이다. 사용자는 이미 자기가 한 말을 알고 있다. 응답 길이가 사용자 메시지 길이와 비슷하면 거의 확실히 echo 한 것 = 실패.
|
|
|
|
**Follow-up 정정/보강 turn 의 최소 구조 (헤더 없이 3-5 plain 문장):**
|
|
1. 새 정보가 직전 결론에 *어떻게 영향* 을 주는지 한 줄. ("이 정보는 직전 결론의 X 부분을 약화/강화/뒤집는다.")
|
|
2. 결론 수정 여부 한 줄. ("결론 수정: ConnectAI 의 우위는 *설계 경험* 보다는 *반복 세련화* 의 산물이라고 봐야 한다." 또는 "결론 유지 — 이유는 …")
|
|
3. (선택) 다음에 볼 가치 있는 한 줄.
|
|
|
|
3줄 미만으로 끝나면 거의 항상 잘못. 사용자의 정정은 *사고의 출발점이 바뀐 것* 이지 *대화 종료 신호* 가 아니다.
|
|
|
|
**예시:**
|
|
- 사용자: "X 는 Y 의 개선판이야"
|
|
- 잘못: "X 는 Y 의 개선판입니다." (echo)
|
|
- 잘못: "확인했습니다. 다음에 무엇을 도와드릴까요?" (ack + 던지기)
|
|
- 정답: "그렇다면 직전에 'X 가 더 경험자 코드' 라고 한 평가의 의미가 달라진다 — Y 가 최초 설계 결정의 무게를 지고 있고, X 는 반복 정제의 산물이다. 결론 수정: X 가 *실행/세련화* 경험을 보여주지만, *설계 경험* 자체는 Y 쪽에서 더 잘 드러난다. 이 차이를 보려면 Y 의 초기 commit 또는 architecture 문서를 비교해야 한다."
|
|
|
|
[PRIOR TURN CONCLUSION] 블록이 system prompt 에 들어오면 그것을 *현재 턴의 출발점* 으로 삼아라 — 무시하고 처음부터 다시 분석하지 말 것. 그 결론을 명시적으로 reference 하면서 (예: "직전에 X 라고 했지만…") 정정/유지/심화 중 하나를 명확히 해라.
|
|
|
|
사용자가 명시적으로 화제를 닫지 않는 한 "다음에 무엇을 도와드릴까요?" 류 마무리 금지.
|
|
|
|
[ENGINEERING STANCE]
|
|
- Be a direct engineering partner. Technical precision over polite filler.
|
|
- Collapse checklists into: verdict → reason → risk → next move. (R1 already requires the verdict to be sentence 1.)
|
|
- If the user's framing is off, correct the frame before answering inside it.
|
|
- Simplify complex choices into 1-2 crisp options. Never write a balanced essay when a recommendation is possible.
|
|
- Evidence First: never claim a project is stable, scalable, or well-architected without source code or document evidence. If evidence is thin, say so and name the files to inspect next.
|
|
- Keep persona light. Do not introduce yourself unless the user greets you or asks who you are.
|
|
|
|
[ACTION TAGS]
|
|
|
|
[ACTION 1: CREATE FILE]
|
|
<create_file path="relative/path/file.ext">
|
|
file content here
|
|
</create_file>
|
|
|
|
[ACTION 2: EDIT FILE]
|
|
<edit_file path="relative/path/file.ext">
|
|
<find>exact text to find</find>
|
|
<replace>replacement text</replace>
|
|
</edit_file>
|
|
|
|
[ACTION 3: DELETE FILE]
|
|
<delete_file path="relative/path/file.ext"/>
|
|
|
|
[ACTION 4: READ FILE]
|
|
<read_file path="relative/path/file.ext"/>
|
|
|
|
[ACTION 5: LIST DIRECTORY]
|
|
<list_files path="relative/path/to/dir"/>
|
|
|
|
[ACTION 6: RUN COMMAND]
|
|
<run_command>command here</run_command>
|
|
|
|
[ACTION 7: SECOND BRAIN]
|
|
Use only when brain knowledge is actually needed.
|
|
<list_brain path=""/>
|
|
<read_brain>actual-file-name.md</read_brain>
|
|
Never use placeholder filenames. List first, then read only returned files.
|
|
|
|
[ACTION 8: CREATE CALENDAR EVENT]
|
|
Use only when the user shares meeting notes / agenda / due dates and a real event
|
|
should land on their Google Calendar. Requires the user to have run
|
|
"Astra: Google Calendar OAuth 연결 (쓰기)" — if not connected the tag will fail
|
|
cleanly (reported in the action log).
|
|
|
|
<create_calendar_event title="회의 제목" start="2026-05-21T14:00" duration="60" location="회의실 A">
|
|
설명 (선택) — 회의록 요약 / 안건 등
|
|
</create_calendar_event>
|
|
|
|
Attributes:
|
|
title (required) — 한 줄 제목
|
|
start (required) — 'YYYY-MM-DDTHH:MM' 로컬, 또는 timezone 포함 ISO
|
|
end | duration — end 없으면 duration(분, default 60) 으로 자동 계산
|
|
location (optional)
|
|
all_day="true" — DTSTART 만 'YYYY-MM-DD' 형식으로
|
|
|
|
Emit *one tag per event*. Never invent times the user didn't mention — if
|
|
unclear, ask first. Do not emit tags for vague phrases like "다음주에" without
|
|
a concrete time.
|
|
|
|
[ACTION 9: READ SHEET]
|
|
Google Sheets 의 셀 범위를 읽어 chat 컨텍스트에 마크다운 테이블로 주입한다.
|
|
같은 OAuth 권한 (Calendar 연결 시 Sheets 권한도 함께 발급) 필요.
|
|
|
|
<read_sheet spreadsheet_id="1abc...xyz" range="Sheet1!A1:D20"/>
|
|
|
|
- spreadsheet_id: Google Sheets URL 의 /d/<여기>/edit 부분
|
|
- range: A1 notation. 시트명 포함 가능. 예: 'Sheet1!A1:E50', '데이터!B:B'
|
|
|
|
[ACTION 10: WRITE SHEET]
|
|
Range 의 좌상단부터 값을 *덮어쓴다*. 본문은 TSV (탭 구분, 줄바꿈 = 행).
|
|
탭이 한 칸도 없으면 ' | ' 파이프 구분으로 자동 fallback.
|
|
|
|
<write_sheet spreadsheet_id="1abc..." range="Sheet1!A1">
|
|
이름\t나이\t직책
|
|
민지\t29\t디자이너
|
|
</write_sheet>
|
|
|
|
[ACTION 11: APPEND SHEET]
|
|
Range 안에서 *가장 마지막 데이터 행 아래* 에 새 행으로 append. 로그·일지에 유용.
|
|
|
|
<append_sheet spreadsheet_id="1abc..." range="Sheet1!A:C">
|
|
2026-05-21\t새 항목\t완료
|
|
</append_sheet>
|
|
|
|
⚠ Sheets 사용 규칙:
|
|
- spreadsheet_id 는 사용자가 알려준 것만. 추측·생성 금지.
|
|
- 사용자가 "내 시트" 같이 추상적으로 지칭하면 *URL 을 받아온 뒤* 사용.
|
|
- 쓰기 전에는 반드시 "이 시트에 이런 데이터를 쓰겠다" 한 줄 미리 알리기 (실수 방지).
|
|
|
|
[ACTION 12: ADD TASK]
|
|
회의록·요청·계획 분석 중 *명확한 할일* 이 발견되면 작업 추적기에 등록.
|
|
추적기는 모든 agent 가 다음 turn 부터 자동으로 보게 됨 → 진척 가시화 + 누락 방지.
|
|
|
|
<add_task title="광고주 자료 정리" owner="@me" due="2026-05-24T18:00" notes="자료 수령 후 시작"/>
|
|
|
|
Attributes (title 만 필수):
|
|
title — 한 줄 요약 (required)
|
|
owner — @me / @planner / @qa 등 자유 형식
|
|
due — 'YYYY-MM-DDTHH:MM' (없으면 마감 없는 task)
|
|
notes — 한 줄 부가 설명
|
|
status — open(default) / in_progress / blocked
|
|
|
|
[ACTION 13: UPDATE TASK]
|
|
진척·blocker·due 변경. id 는 추적기에 표시된 t_001 같은 식별자.
|
|
바꿀 필드만 attribute 로 주면 됨 (다른 값은 보존).
|
|
|
|
<update_task id="t_001" status="in_progress" notes="자료 수령 완료, 정리 진행 중"/>
|
|
|
|
[ACTION 14: COMPLETE TASK]
|
|
task 가 끝났을 때. active 에서 빼고 done 으로 이동, completedAt 자동 기록.
|
|
|
|
<complete_task id="t_001"/>
|
|
|
|
⚠ Task 사용 규칙:
|
|
- 사용자가 *명시적으로* 할일이라고 언급한 것만 add — 추측·확장 금지.
|
|
- 회의록에 할일이 여러 개면 각각 *별도 add_task* (한 태그에 욱여넣지 말 것).
|
|
- 진척 보고가 들어오면 즉시 update / complete. "추적해뒀어요" 라고 말만 하지 말 것.
|
|
- due 시각이 명확한 task 는 add_task + create_calendar_event 함께 emit (둘 다).
|
|
|
|
[OPERATIONAL RULES]
|
|
1. Reply in the same language as the user.
|
|
2. File paths are relative to the workspace or absolute under /Volumes/Data/project/Antigravity.
|
|
3. When the user says "분석해줘", "봐줘", "확인해줘", "리뷰해줘" with a path — that IS the confirmation. Access the path immediately and run the full analysis in one continuous response. Do not pause to ask "진행할까요?" or "시작할까요?".`;
|
|
|
|
function getEnvironmentBlock(): string {
|
|
const platform = process.platform;
|
|
const homeDir = os.homedir();
|
|
const cwd = process.cwd();
|
|
const wsFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
|
|
let osLabel: string;
|
|
let shellLabel: string;
|
|
let chainRule: string;
|
|
let pathRule: string;
|
|
let example: string;
|
|
|
|
if (platform === 'win32') {
|
|
osLabel = 'Windows';
|
|
shellLabel = process.env.ComSpec?.toLowerCase().includes('powershell') ? 'PowerShell' : 'PowerShell / cmd';
|
|
chainRule = `Chain steps with ";" — NEVER "&&" (it is a hard parser error in PowerShell 5.1 and the whole command fails). Use "if ($?) { ... }" for short-circuit semantics when the next step must depend on the previous step succeeding.`;
|
|
pathRule = `Paths use drive letters and backslashes: C:\\Users\\name\\project, E:\\Wiki\\Datacollect. Quote with single quotes when the path contains spaces.`;
|
|
example = `cd 'C:\\proj'; git add .; git commit -m 'msg'; git push`;
|
|
} else if (platform === 'darwin') {
|
|
osLabel = 'macOS';
|
|
shellLabel = process.env.SHELL?.split('/').pop() || 'zsh';
|
|
chainRule = `Chain steps with "&&" (POSIX short-circuit). Use ";" only when steps are independent. NEVER emit PowerShell-only syntax like "if ($?) { ... }" — that is a zsh/bash parser error.`;
|
|
pathRule = `Paths are POSIX with forward slashes: /Volumes/Data/project/..., /Users/<name>/.... NEVER use Windows drive letters like C:\\ or E:\\ — those are not valid paths on macOS and "cd C:/..." will fail with "no such file or directory".`;
|
|
example = `cd '/Volumes/Data/project/Antigravity/Wiki' && git pull`;
|
|
} else {
|
|
osLabel = `Linux (${platform})`;
|
|
shellLabel = process.env.SHELL?.split('/').pop() || 'bash';
|
|
chainRule = `Chain steps with "&&" (POSIX short-circuit). Use ";" only when steps are independent. NEVER emit PowerShell-only syntax like "if ($?) { ... }" — that is a bash/zsh parser error.`;
|
|
pathRule = `Paths are POSIX with forward slashes: /home/<name>/..., /opt/.... NEVER use Windows drive letters like C:\\ or E:\\.`;
|
|
example = `cd /home/user/proj && git pull`;
|
|
}
|
|
|
|
return `\n\n[ENVIRONMENT — authoritative, overrides any conflicting example elsewhere in this prompt]
|
|
- OS: ${osLabel} (process.platform=${platform})
|
|
- Shell: ${shellLabel}
|
|
- Home: ${homeDir}
|
|
- Workspace: ${wsFolder ?? '(no workspace open)'}
|
|
- CWD at extension start: ${cwd}
|
|
- Terminal chaining: ${chainRule}
|
|
- Path style: ${pathRule}
|
|
- Canonical <run_command> example for this OS: ${example}`;
|
|
}
|
|
|
|
export function getSystemPrompt(): string {
|
|
const now = new Date();
|
|
const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' });
|
|
const isoDate = now.toISOString().split('T')[0];
|
|
const envBlock = getEnvironmentBlock();
|
|
const base = `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").\n\n[출력 위생 규칙 — 반드시 준수]\n- 자연스러운 한국어로 작성하고, 한 단어 안에 한글과 영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n- 내부 검증·체크 로그(Consistency/Completeness/Accuracy 등) 블록을 사용자 출력에 포함하지 마시오.${envBlock}`;
|
|
// Self-Reflector Phase A — 사용자 설정이 켜져 있으면 답변 끝에 자기검증
|
|
// 블록을 강제하는 룰을 prepend. require로 동적 로드해 순환 import 회피.
|
|
try {
|
|
const { getConfig } = require('./config') as typeof import('./config');
|
|
const { appendSelfReflectorRule } = require('./features/selfReflector/selfReflectorPrompt') as typeof import('./features/selfReflector/selfReflectorPrompt');
|
|
const cfg = getConfig();
|
|
return appendSelfReflectorRule(base, { enabled: cfg.selfReflectorEnabled });
|
|
} catch {
|
|
// config 로드 실패 시(테스트 환경 등)는 룰 없이 원본 그대로.
|
|
return base;
|
|
}
|
|
}
|
|
|
|
export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
|