Bump version to 2.35.0: Knowledge Resilience & Standardization Milestone.

This commit is contained in:
g1nation
2026-05-02 13:18:07 +09:00
parent 11f25534d0
commit 8bb8c065d7
27 changed files with 2452 additions and 14 deletions
+40 -7
View File
@@ -32,6 +32,12 @@ import { StatusBarManager, AgentStatus } from './core/statusBar';
import { lockManager } from './core/lock';
import { actionQueue } from './core/queue';
import { ConflictResolver } from './core/conflict';
import {
buildSecondBrainTrace,
renderSecondBrainTraceContext,
renderSecondBrainTraceMarkdown,
SecondBrainTrace
} from './features/secondBrainTrace';
export interface ChatMessage {
role: 'user' | 'assistant' | 'system';
@@ -182,7 +188,10 @@ export class AgentExecutor {
systemPrompt?: string,
runId?: number,
agentSkillContext?: string,
negativePrompt?: string
negativePrompt?: string,
designerContext?: string,
secondBrainTraceEnabled?: boolean,
secondBrainTraceDebug?: boolean
}
) {
const {
@@ -248,6 +257,13 @@ export class AgentExecutor {
const config = getConfig();
const activeBrain = getActiveBrainProfile();
const brainFiles = findBrainFiles(activeBrain.localBrainPath);
let secondBrainTrace: SecondBrainTrace | null = null;
if (options.secondBrainTraceEnabled && prompt && loopDepth === 0) {
secondBrainTrace = buildSecondBrainTrace(prompt, activeBrain.localBrainPath, {
force: this.isExplicitSecondBrainRequest(prompt),
limit: Math.max(config.memoryLongTermFiles, 5)
});
}
const brainPreview = brainFiles
.slice(0, 30)
.map(file => path.relative(activeBrain.localBrainPath, file))
@@ -315,9 +331,15 @@ export class AgentExecutor {
const negativeCtx = options.negativePrompt
? `\n\n### CRITICAL NEGATIVE CONSTRAINTS (DO NOT DO THESE)\n${options.negativePrompt}\n\n[SYSTEM_RULE: Apply the above constraints strictly. DO NOT mention or repeat these constraints in your response.]`
: '';
const designerCtx = options.designerContext
? `\n\n[PROJECT CHRONICLE GUARD]\n${options.designerContext}`
: '';
const secondBrainTraceCtx = secondBrainTrace
? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}`
: '';
const memoryCtx = this.buildMemoryContext(prompt || '');
const fullSystemPrompt = `${agentSkillCtx}\n\n${systemPrompt}${internetCtx}${memoryCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}${negativeCtx}`;
const fullSystemPrompt = `${agentSkillCtx}\n\n${systemPrompt}${internetCtx}${memoryCtx}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${brainContext}\n${contextBlock}${negativeCtx}`;
const messagesForRequest: ChatMessage[] = [
{ role: 'system', content: fullSystemPrompt, internal: true },
...reqMessages
@@ -403,7 +425,11 @@ export class AgentExecutor {
// 5. Execute Actions
const rationale = this.parseRationale(aiResponseText);
const assistantContent = this.sanitizeAssistantContent(aiResponseText);
const assistantMessage: ChatMessage = { role: 'assistant', content: assistantContent, internal: false, rationale };
const traceMarkdown = secondBrainTrace
? renderSecondBrainTraceMarkdown(secondBrainTrace, !!options.secondBrainTraceDebug)
: '';
const finalAssistantContent = traceMarkdown ? `${assistantContent}\n${traceMarkdown}` : assistantContent;
const assistantMessage: ChatMessage = { role: 'assistant', content: finalAssistantContent, internal: false, rationale };
this.chatHistory.push(assistantMessage);
this.statusBarManager.updateStatus(AgentStatus.Executing);
@@ -426,9 +452,9 @@ export class AgentExecutor {
if (report.length === 0 && loopDepth === 0 && this.isUnproductiveWaitingReply(assistantContent)) {
assistantMessage.internal = false;
const correctedReply = await this.buildUnproductiveReplyCorrection(prompt || '');
assistantMessage.content = correctedReply;
assistantMessage.content = traceMarkdown ? `${correctedReply}\n${traceMarkdown}` : correctedReply;
this.emitHistoryChanged();
this.webview.postMessage({ type: 'streamChunk', value: correctedReply });
this.webview.postMessage({ type: 'streamChunk', value: assistantMessage.content });
return;
}
@@ -462,7 +488,7 @@ export class AgentExecutor {
this.emitHistoryChanged();
this.statusBarManager.updateStatus(AgentStatus.Success);
this.webview.postMessage({ type: 'streamChunk', value: assistantContent });
this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent });
} catch (error: any) {
this.statusBarManager.updateStatus(AgentStatus.Error, error.message);
@@ -517,12 +543,15 @@ export class AgentExecutor {
const selectedAgentContext = options.agentSkillContext
? `\nSelected Agent Reference:\n${options.agentSkillContext}`
: '';
const designerContext = options.designerContext
? `\nProject Chronicle Guard:\n${options.designerContext}`
: '';
// 워크플로우 매니저에게 설정 기반 실행 위임
const finalReport = await AgentWorkflowManager.runStrictWorkflow(
prompt,
modelName,
`${brainContext}${selectedAgentContext}`,
`${brainContext}${selectedAgentContext}${designerContext}`,
signal,
(step, msg) => {
this.webview?.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
@@ -614,6 +643,10 @@ export class AgentExecutor {
return mentionsBrain && asksOverview;
}
private isExplicitSecondBrainRequest(prompt: string): boolean {
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|노트|문서|참고해서|사용해서|검색해서|근거|출처)/i.test(prompt);
}
private buildBrainOverviewReply(): string {
const activeBrain = getActiveBrainProfile();
const brainDir = activeBrain.localBrainPath;
@@ -0,0 +1,50 @@
import { ProjectProfile } from './types';
export function buildProjectChronicleGuardContext(project: ProjectProfile | null): string {
const hasUsableProject = !!project?.recordRoot?.trim();
const projectLines = project ? [
`Project selection status: selected`,
`Active record project: ${project.projectName}`,
`Project root: ${project.projectRoot || 'Not set'}`,
`Record root: ${project.recordRoot}`,
`Core purpose: ${project.corePurpose || project.description || 'Not captured yet.'}`,
`Record detail level: ${project.detailLevel}`
] : [
'Project selection status: not selected',
'No active record project is selected. Before writing records, ask the user to select or create one.'
];
return [
...projectLines,
'',
'This guard is active for project ideas, feature requests, architecture proposals, implementation planning, and design decisions.',
'',
'Required response order for new ideas or feature requests:',
'1. Request summary.',
'2. Inferred user intent.',
'3. Project record target check. If no project is selected, ask whether to use an existing project, create a new project, or skip recording.',
'4. Record path check. If no record root is available, say a Markdown record path is required before writing files.',
'5. Ask only 1 to 3 blocking questions.',
'6. For every question, include "Question reason" explaining why it changes storage, scope, dependencies, or implementation direction.',
'7. Direction review focused on project fit and dependency risk.',
'8. Recommend a low-dependency MVP first.',
'9. Put Vector DB, relational DB, knowledge graph, semantic search, and complex automation only under "Later expansion" unless the user explicitly asks for them now.',
'10. End with "Candidate records for this discussion" and list planning, discussions, decisions, development, bugs, or retrospectives paths as candidates.',
'',
'Decision policy:',
'- Do not mark a decision as accepted until the user confirms it.',
'- Before confirmation, call decisions "candidates" or "pending".',
'- Prefer "reduced adoption" when the idea is useful but too large for the MVP.',
'',
'Tone and scope:',
'- Be practical and plain-spoken.',
'- Avoid grand phrases like advanced cognitive architecture, compounding knowledge, perfect graph, or ultimate knowledge distiller.',
'- When the user wants low dependency, keep the first proposal to Markdown, JSON, local files, and explicit user save actions.',
'- Do not jump directly to large architectures. Narrow direction before expanding.',
'',
hasUsableProject
? 'The current project and record root are available, so candidate record paths may use this project.'
: 'The project or record root is missing, so candidate records must be described but not written.',
'Do not claim a Markdown file was written unless a tool or explicit sidebar action actually wrote it.'
].join('\n');
}
+189
View File
@@ -0,0 +1,189 @@
import * as fs from 'fs';
import * as path from 'path';
import { MarkdownFileWriter } from './markdownFileWriter';
import {
BugRecord,
ChronicleRecordEntry,
ChronicleWriteResult,
DecisionRecord,
DevelopmentLog,
DiscussionRecord,
PlanningDocument,
ProjectProfile,
RetrospectiveRecord
} from './types';
import {
renderBugRecord,
renderDecisionRecord,
renderDevelopmentLog,
renderDiscussionRecord,
renderPlanningDocument,
renderProjectProfile,
renderProjectReadme,
renderRetrospective,
renderTimelineSeed
} from './templates';
export * from './types';
export * from './guardPrompt';
const sectionDirs = ['planning', 'discussions', 'decisions', 'development', 'bugs', 'retrospectives'];
export class ProjectChronicleManager {
private readonly writer = new MarkdownFileWriter();
public ensureProject(profile: ProjectProfile): void {
if (!profile.recordRoot || !profile.recordRoot.trim()) {
throw new Error('Record root is required before writing chronicle documents.');
}
this.writer.ensureDir(profile.recordRoot);
for (const dir of sectionDirs) {
this.writer.ensureDir(path.join(profile.recordRoot, dir));
}
this.writeSeedFile(path.join(profile.recordRoot, 'README.md'), renderProjectReadme(profile));
this.writeSeedFile(path.join(profile.recordRoot, 'project-profile.md'), renderProjectProfile(profile));
this.writeSeedFile(path.join(profile.recordRoot, 'timeline.md'), renderTimelineSeed(profile));
this.writeProjectConfig(profile);
}
public writePlanning(profile: ProjectProfile, doc: PlanningDocument): ChronicleWriteResult {
this.ensureProject(profile);
const date = this.datePart(doc.createdAt);
const fileName = `${date}_${this.slug(doc.featureName)}.md`;
return this.write(profile, 'planning', fileName, renderPlanningDocument(doc));
}
public writeDiscussion(profile: ProjectProfile, record: DiscussionRecord): ChronicleWriteResult {
this.ensureProject(profile);
const date = this.datePart(record.createdAt);
const fileName = `${date}_${this.slug(record.title)}.md`;
return this.write(profile, 'discussions', fileName, renderDiscussionRecord(record));
}
public writeDecision(profile: ProjectProfile, record: DecisionRecord, adrNumber: number): ChronicleWriteResult {
this.ensureProject(profile);
const padded = String(adrNumber).padStart(4, '0');
const fileName = `ADR-${padded}-${this.slug(record.title)}.md`;
return this.write(profile, 'decisions', fileName, renderDecisionRecord(record));
}
public writeDevelopmentLog(profile: ProjectProfile, log: DevelopmentLog): ChronicleWriteResult {
this.ensureProject(profile);
const date = this.datePart(log.createdAt);
const fileName = `${date}_${this.slug(log.featureName)}_implementation.md`;
return this.write(profile, 'development', fileName, renderDevelopmentLog(log));
}
public writeBug(profile: ProjectProfile, record: BugRecord, bugNumber: number): ChronicleWriteResult {
this.ensureProject(profile);
const padded = String(bugNumber).padStart(4, '0');
const fileName = `BUG-${padded}-${this.slug(record.title)}.md`;
return this.write(profile, 'bugs', fileName, renderBugRecord(record));
}
public writeRetrospective(profile: ProjectProfile, record: RetrospectiveRecord): ChronicleWriteResult {
this.ensureProject(profile);
const date = this.datePart(record.createdAt);
const fileName = `${date}_${this.slug(record.title)}.md`;
return this.write(profile, 'retrospectives', fileName, renderRetrospective(record));
}
public appendTimeline(profile: ProjectProfile, lines: string[], createdAt: string = new Date().toISOString()): void {
this.ensureProject(profile);
const timelinePath = path.join(profile.recordRoot, 'timeline.md');
const markdown = [
'',
`## ${this.datePart(createdAt)}`,
...lines.map(line => `- ${line}`)
].join('\n');
this.writer.appendMarkdown(timelinePath, markdown);
}
public nextAdrNumber(profile: ProjectProfile): number {
return this.nextNumber(path.join(profile.recordRoot, 'decisions'), /^ADR-(\d+)/);
}
public nextBugNumber(profile: ProjectProfile): number {
return this.nextNumber(path.join(profile.recordRoot, 'bugs'), /^BUG-(\d+)/);
}
public listRecords(profile: ProjectProfile): ChronicleRecordEntry[] {
this.ensureProject(profile);
const records: ChronicleRecordEntry[] = [];
for (const section of sectionDirs) {
const sectionPath = path.join(profile.recordRoot, section);
if (!fs.existsSync(sectionPath)) continue;
for (const fileName of fs.readdirSync(sectionPath)) {
if (!fileName.endsWith('.md')) continue;
const filePath = path.join(sectionPath, fileName);
const stat = fs.statSync(filePath);
records.push({
section,
fileName,
filePath,
relativePath: path.relative(profile.recordRoot, filePath),
updatedAt: stat.mtimeMs
});
}
}
return records.sort((a, b) => b.updatedAt - a.updatedAt);
}
private write(profile: ProjectProfile, section: string, fileName: string, markdown: string): ChronicleWriteResult {
const filePath = this.writer.writeMarkdown(path.join(profile.recordRoot, section, fileName), markdown);
return {
filePath,
relativePath: path.relative(profile.recordRoot, filePath)
};
}
private writeSeedFile(filePath: string, content: string): void {
if (!fs.existsSync(filePath)) {
this.writer.writeMarkdown(filePath, content);
}
}
private writeProjectConfig(profile: ProjectProfile): void {
const configPath = path.join(profile.recordRoot, 'chronicle.config.json');
const config = {
projectId: profile.projectId,
projectName: profile.projectName,
projectRoot: profile.projectRoot || '',
recordRoot: profile.recordRoot,
description: profile.description || '',
corePurpose: profile.corePurpose || '',
detailLevel: profile.detailLevel,
createdAt: profile.createdAt,
updatedAt: new Date().toISOString()
};
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
}
private nextNumber(dir: string, pattern: RegExp): number {
if (!fs.existsSync(dir)) return 1;
const max = fs.readdirSync(dir).reduce((current, fileName) => {
const match = fileName.match(pattern);
if (!match) return current;
return Math.max(current, Number(match[1]) || 0);
}, 0);
return max + 1;
}
private slug(value: string): string {
const slug = value
.toLowerCase()
.replace(/[^a-z0-9가-힣]+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 60);
return slug || 'record';
}
private datePart(iso?: string): string {
return (iso || new Date().toISOString()).slice(0, 10);
}
}
@@ -0,0 +1,41 @@
import * as fs from 'fs';
import * as path from 'path';
export class MarkdownFileWriter {
public ensureDir(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
public writeMarkdown(filePath: string, content: string): string {
this.ensureDir(path.dirname(filePath));
const finalPath = this.getAvailablePath(filePath);
fs.writeFileSync(finalPath, this.normalize(content), 'utf8');
return finalPath;
}
public appendMarkdown(filePath: string, content: string): void {
this.ensureDir(path.dirname(filePath));
fs.appendFileSync(filePath, this.normalize(content), 'utf8');
}
private getAvailablePath(filePath: string): string {
if (!fs.existsSync(filePath)) return filePath;
const dir = path.dirname(filePath);
const ext = path.extname(filePath);
const base = path.basename(filePath, ext);
let index = 2;
while (true) {
const candidate = path.join(dir, `${base}-${index}${ext}`);
if (!fs.existsSync(candidate)) return candidate;
index += 1;
}
}
private normalize(content: string): string {
return content.replace(/\r\n/g, '\n').trimEnd() + '\n';
}
}
+258
View File
@@ -0,0 +1,258 @@
import {
BugRecord,
DecisionRecord,
DevelopmentLog,
DiscussionRecord,
PlanningDocument,
ProjectProfile,
RetrospectiveRecord
} from './types';
const list = (items: string[] | undefined, fallback: string = 'Not captured yet.') => {
if (!items || items.length === 0) return fallback;
return items.map(item => `- ${item}`).join('\n');
};
const dateOnly = (iso?: string) => (iso || new Date().toISOString()).slice(0, 10);
export function renderProjectReadme(profile: ProjectProfile): string {
return [
`# ${profile.projectName} Chronicle Records`,
'',
'## Project',
`- ID: ${profile.projectId}`,
`- Root: ${profile.projectRoot || 'Not set'}`,
`- Record root: ${profile.recordRoot}`,
`- Detail level: ${profile.detailLevel}`,
'',
'## Purpose',
profile.corePurpose || profile.description || 'Not captured yet.',
'',
'## Folders',
'- `planning/`',
'- `discussions/`',
'- `decisions/`',
'- `development/`',
'- `bugs/`',
'- `retrospectives/`'
].join('\n');
}
export function renderProjectProfile(profile: ProjectProfile): string {
return [
'# Project Profile',
'',
'## Project Name',
profile.projectName,
'',
'## Description',
profile.description || 'Not captured yet.',
'',
'## Project Root',
profile.projectRoot || 'Not set',
'',
'## Record Root',
profile.recordRoot,
'',
'## Core Purpose',
profile.corePurpose || 'Not captured yet.',
'',
'## Target Users',
list(profile.targetUsers),
'',
'## Avoid Directions',
list(profile.avoidDirections),
'',
'## Record Detail Level',
profile.detailLevel,
'',
'## Created',
profile.createdAt,
'',
'## Updated',
profile.updatedAt
].join('\n');
}
export function renderTimelineSeed(profile: ProjectProfile): string {
return [
'# Project Timeline',
'',
`## ${dateOnly(profile.createdAt)}`,
`- Project Chronicle record folder initialized for ${profile.projectName}.`
].join('\n');
}
export function renderPlanningDocument(doc: PlanningDocument): string {
return [
`# Feature Plan: ${doc.featureName}`,
'',
'## 1. Feature Name',
doc.featureName,
'',
'## 2. Reason',
doc.purpose,
'',
'## 3. Original User Request',
doc.sourceRequest || 'Not captured yet.',
'',
'## 4. Interpreted User Intent',
doc.userIntent,
'',
'## 5. Background',
doc.background,
'',
'## 6. Scope',
list(doc.scope),
'',
'## 7. Out Of Scope',
list(doc.outOfScope),
'',
'## 8. Development Direction',
doc.developmentDirection,
'',
'## 9. Dependency Strategy',
doc.dependencyStrategy,
'',
'## 10. Expected Value',
doc.expectedValue,
'',
'## 11. Success Criteria',
list(doc.successCriteria),
'',
'## 12. Developer Instruction',
doc.developerInstruction
].join('\n');
}
export function renderDiscussionRecord(record: DiscussionRecord): string {
const questions = record.questions.length
? record.questions.map((q, index) => [
`### Question ${index + 1}`,
q.question,
'',
'### Reason',
q.reason,
'',
'### Expected Information',
q.expectedInformation,
'',
'### Impact On Decision',
q.impactOnDecision,
'',
q.userAnswer ? `### User Answer\n${q.userAnswer}` : '',
q.result ? `### Result\n${q.result}` : ''
].filter(Boolean).join('\n')).join('\n\n')
: 'No explicit question was captured.';
return [
`# Discussion: ${record.title}`,
'',
'## User Request Summary',
record.userRequest,
'',
'## Interpreted Intent',
record.interpretedIntent,
'',
'## Questions',
questions,
'',
'## Main Discussion',
list(record.discussions),
'',
'## Decisions',
record.decisions.length
? record.decisions.map(decision => `- ${decision.title}: ${decision.decision}`).join('\n')
: 'No decisions captured yet.'
].join('\n');
}
export function renderDecisionRecord(record: DecisionRecord): string {
return [
`# ADR: ${record.title}`,
'',
'## Status',
record.status,
'',
'## Context',
record.context,
'',
'## Decision',
record.decision,
'',
'## Reason',
record.reason,
'',
'## Alternatives',
list(record.alternatives),
'',
'## Consequences',
list(record.consequences)
].join('\n');
}
export function renderDevelopmentLog(log: DevelopmentLog): string {
return [
`# Development Log: ${log.featureName}`,
'',
'## Purpose',
log.purpose,
'',
'## Implementation Summary',
log.implementationSummary,
'',
'## Architecture',
log.architecture,
'',
'## Changed Files',
list(log.changedFiles),
'',
'## Dependency Notes',
log.dependencyNotes,
'',
'## Bugs',
log.bugs.length ? log.bugs.map(bug => `- ${bug.title}: ${bug.symptom}`).join('\n') : 'No bugs recorded.',
'',
'## Lessons',
list(log.lessons)
].join('\n');
}
export function renderBugRecord(record: BugRecord): string {
return [
`# Bug: ${record.title}`,
'',
'## Date',
dateOnly(record.createdAt),
'',
'## Symptom',
record.symptom,
'',
'## Cause',
record.cause,
'',
'## Fix',
record.fix,
'',
'## Prevention',
record.prevention
].join('\n');
}
export function renderRetrospective(record: RetrospectiveRecord): string {
return [
`# Retrospective: ${record.title}`,
'',
'## Summary',
record.summary,
'',
'## Went Well',
list(record.wentWell),
'',
'## To Improve',
list(record.toImprove),
'',
'## Next Actions',
list(record.nextActions)
].join('\n');
}
+104
View File
@@ -0,0 +1,104 @@
export type ChronicleDetailLevel = 'simple' | 'standard' | 'detailed';
export interface ProjectProfile {
projectId: string;
projectName: string;
projectRoot?: string;
recordRoot: string;
description?: string;
corePurpose?: string;
targetUsers?: string[];
avoidDirections?: string[];
detailLevel: ChronicleDetailLevel;
createdAt: string;
updatedAt: string;
}
export interface QuestionRecord {
question: string;
reason: string;
expectedInformation: string;
impactOnDecision: string;
userAnswer?: string;
result?: string;
}
export interface DecisionRecord {
title: string;
status: 'accepted' | 'rejected' | 'deferred' | 'changed';
context: string;
decision: string;
reason: string;
alternatives?: string[];
consequences?: string[];
createdAt?: string;
}
export interface PlanningDocument {
featureName: string;
purpose: string;
background: string;
userIntent: string;
scope: string[];
outOfScope: string[];
developmentDirection: string;
dependencyStrategy: string;
expectedValue: string;
successCriteria: string[];
developerInstruction: string;
sourceRequest?: string;
createdAt?: string;
}
export interface DiscussionRecord {
title: string;
userRequest: string;
interpretedIntent: string;
questions: QuestionRecord[];
discussions: string[];
decisions: DecisionRecord[];
createdAt?: string;
}
export interface DevelopmentLog {
featureName: string;
purpose: string;
implementationSummary: string;
architecture: string;
changedFiles: string[];
dependencyNotes: string;
bugs: BugRecord[];
lessons: string[];
createdAt?: string;
}
export interface BugRecord {
title: string;
symptom: string;
cause: string;
fix: string;
prevention: string;
createdAt?: string;
}
export interface RetrospectiveRecord {
title: string;
summary: string;
wentWell: string[];
toImprove: string[];
nextActions: string[];
createdAt?: string;
}
export interface ChronicleWriteResult {
filePath: string;
relativePath: string;
}
export interface ChronicleRecordEntry {
section: string;
fileName: string;
filePath: string;
relativePath: string;
updatedAt: number;
}
+264
View File
@@ -0,0 +1,264 @@
import * as fs from 'fs';
import * as path from 'path';
import { findBrainFiles, summarizeText } from '../utils';
export interface SecondBrainTraceDocument {
title: string;
path: string;
absolutePath: string;
score: number;
excerpt: string;
usedInAnswer: boolean;
usedFor?: string;
excludedReason?: string;
}
export interface SecondBrainTrace {
userQuery: string;
shouldUseSecondBrain: boolean;
secondBrainUsed: boolean;
reason: string;
retrievalQuery: string;
searchedCollections: string[];
retrievedDocuments: SecondBrainTraceDocument[];
groundingScore: number;
}
export function buildSecondBrainTrace(userQuery: string, brainRoot: string, options: {
force?: boolean;
limit?: number;
} = {}): SecondBrainTrace {
const query = userQuery.trim();
const shouldUseSecondBrain = !!options.force || shouldUseBrain(query);
const retrievalQuery = buildRetrievalQuery(query);
const baseTrace: SecondBrainTrace = {
userQuery: query,
shouldUseSecondBrain,
secondBrainUsed: false,
reason: shouldUseSecondBrain
? 'Project-specific or memory-sensitive information may be needed.'
: 'This looks answerable without project-specific Second Brain context.',
retrievalQuery,
searchedCollections: [],
retrievedDocuments: [],
groundingScore: 0
};
if (!shouldUseSecondBrain) return baseTrace;
if (!brainRoot || !fs.existsSync(brainRoot)) {
return {
...baseTrace,
reason: 'Second Brain was requested, but the active brain folder does not exist.'
};
}
const files = findBrainFiles(brainRoot);
const terms = tokenize(retrievalQuery);
const scored = files.map((file) => scoreFile(file, brainRoot, terms))
.filter((doc) => doc.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, options.limit || 5);
const usedDocs = scored.slice(0, Math.min(3, scored.length)).map((doc) => ({
...doc,
usedInAnswer: true,
usedFor: inferUsedFor(doc.excerpt)
}));
const unusedDocs = scored.slice(usedDocs.length).map((doc) => ({
...doc,
usedInAnswer: false,
excludedReason: 'Lower relevance than the documents selected as answer context.'
}));
const retrievedDocuments = [...usedDocs, ...unusedDocs];
const usedCount = retrievedDocuments.filter((doc) => doc.usedInAnswer).length;
return {
...baseTrace,
secondBrainUsed: retrievedDocuments.length > 0,
reason: retrievedDocuments.length > 0
? 'Relevant Markdown notes were found and selected as answer context.'
: 'Second Brain search ran, but no sufficiently relevant Markdown notes were found.',
searchedCollections: inferCollections(retrievedDocuments),
retrievedDocuments,
groundingScore: retrievedDocuments.length === 0
? 0
: Number((usedCount / retrievedDocuments.length).toFixed(2))
};
}
export function renderSecondBrainTraceContext(trace: SecondBrainTrace): string {
if (!trace.shouldUseSecondBrain) {
return [
'[SECOND BRAIN TRACE]',
'Second Brain was not used for this request.',
`Reason: ${trace.reason}`,
'If the user explicitly asks to use Second Brain or asks project-specific memory questions, use it.'
].join('\n');
}
const docs = trace.retrievedDocuments
.filter((doc) => doc.usedInAnswer)
.map((doc) => [
`- ${doc.path}`,
` Score: ${doc.score}`,
` Relevant content: ${doc.excerpt}`
].join('\n'))
.join('\n');
return [
'[SECOND BRAIN TRACE]',
`Second Brain used: ${trace.secondBrainUsed ? 'yes' : 'no'}`,
`Retrieval query: ${trace.retrievalQuery}`,
`Reason: ${trace.reason}`,
docs ? `Selected notes:\n${docs}` : 'Selected notes: none',
'',
'When answering, use only selected notes that are relevant. If these notes influence the answer, mention them in the final reference section.'
].join('\n');
}
export function renderSecondBrainTraceMarkdown(trace: SecondBrainTrace, debug: boolean = false): string {
const usedDocs = trace.retrievedDocuments.filter((doc) => doc.usedInAnswer);
const unusedDocs = trace.retrievedDocuments.filter((doc) => !doc.usedInAnswer);
const usedText = usedDocs.length
? usedDocs.map((doc) => [
`- \`${doc.path}\``,
` - Score: ${doc.score}`,
` - 참고 내용: ${doc.excerpt}`
].join('\n')).join('\n')
: '- 없음';
const unusedText = unusedDocs.length
? unusedDocs.map((doc) => [
`- \`${doc.path}\``,
` - 제외 이유: ${doc.excludedReason || '이번 답변의 핵심 근거로 선택되지 않았습니다.'}`
].join('\n')).join('\n')
: '- 없음';
const sections = [
'',
'## 2nd Brain 사용 여부',
trace.secondBrainUsed ? '사용함' : '사용하지 않음',
'',
'## 이유',
trace.reason,
'',
'## 참고한 2nd Brain 문서',
usedText,
'',
'## 검색했지만 사용하지 않은 문서',
unusedText,
'',
'## 참고 품질',
`- 검색된 노트: ${trace.retrievedDocuments.length}`,
`- 실제 사용된 노트: ${usedDocs.length}`,
`- 답변 근거도: ${trace.groundingScore}`
];
if (debug) {
sections.push(
'',
'## Second Brain Debug JSON',
'```json',
JSON.stringify({
secondBrainUsed: trace.secondBrainUsed,
shouldUseSecondBrain: trace.shouldUseSecondBrain,
retrievalQuery: trace.retrievalQuery,
searchedCollections: trace.searchedCollections,
retrievedDocuments: trace.retrievedDocuments.map((doc) => ({
path: doc.path,
score: doc.score,
usedInAnswer: doc.usedInAnswer,
usedFor: doc.usedFor,
excludedReason: doc.excludedReason
})),
groundingScore: trace.groundingScore
}, null, 2),
'```'
);
}
return sections.join('\n');
}
function shouldUseBrain(query: string): boolean {
const normalized = query.toLowerCase();
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|문서|노트|내가|우리|프로젝트|결정|adr|chronicle|가드|설계 원칙|mvp|제외|왜)/i.test(normalized);
}
function buildRetrievalQuery(query: string): string {
return tokenize(query).slice(0, 16).join(' ');
}
function tokenize(value: string): string[] {
const stopWords = new Set(['그리고', '그런데', '해서', '하는', '있어', 'what', 'how', 'the', 'and', 'for', 'with']);
return value
.toLowerCase()
.split(/[^a-z0-9가-힣_]+/g)
.map((term) => term.trim())
.filter((term) => term.length >= 2 && !stopWords.has(term));
}
function scoreFile(file: string, brainRoot: string, terms: string[]): SecondBrainTraceDocument {
const relative = path.relative(brainRoot, file);
const title = path.basename(file, path.extname(file));
const basename = relative.toLowerCase();
let content = '';
try {
content = fs.readFileSync(file, 'utf8');
} catch {
content = '';
}
const lower = content.toLowerCase();
let score = 0;
for (const term of terms) {
if (basename.includes(term)) score += 4;
const matches = lower.split(term).length - 1;
if (matches > 0) score += Math.min(matches, 6);
}
return {
title,
path: relative,
absolutePath: file,
score: Number((score / Math.max(terms.length, 1)).toFixed(2)),
excerpt: summarizeText(bestExcerpt(content, terms), 420),
usedInAnswer: false
};
}
function bestExcerpt(content: string, terms: string[]): string {
const paragraphs = content
.split(/\n\s*\n/g)
.map((part) => part.replace(/\s+/g, ' ').trim())
.filter(Boolean);
if (paragraphs.length === 0) return '';
let best = paragraphs[0];
let bestScore = -1;
for (const paragraph of paragraphs) {
const lower = paragraph.toLowerCase();
const score = terms.reduce((sum, term) => sum + (lower.includes(term) ? 1 : 0), 0);
if (score > bestScore) {
best = paragraph;
bestScore = score;
}
}
return best;
}
function inferCollections(docs: SecondBrainTraceDocument[]): string[] {
const collections = new Set<string>();
for (const doc of docs) {
const first = doc.path.split(/[\\/]/)[0];
if (first) collections.add(first);
}
return Array.from(collections);
}
function inferUsedFor(excerpt: string): string {
if (/의존|coupl|독립|분리/i.test(excerpt)) return '의존도와 독립 모듈 판단';
if (/markdown|마크다운/i.test(excerpt)) return 'Markdown 기반 저장 방향';
if (/질문|의도|reason/i.test(excerpt)) return '질문 의도와 기록 방식';
if (/mvp|제외|scope/i.test(excerpt)) return 'MVP 범위 판단';
return '프로젝트 고유 맥락 확인';
}
+761 -4
View File
@@ -15,6 +15,7 @@ import {
import { getConfig } from './config';
import { AgentExecutor, ChatMessage } from './agent';
import { BridgeInterface } from './bridge';
import { buildProjectChronicleGuardContext, ProjectChronicleManager, ProjectProfile } from './features/projectChronicle';
interface LastVisibleChatSnapshot {
history: ChatMessage[];
@@ -42,10 +43,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
private static readonly lastVisibleChatStateKey = 'g1nation.lastVisibleChat';
private static readonly blankChatStateKey = 'g1nation.blankChatActive';
private static readonly lastAgentStateKey = 'g1nation.lastAgentPath';
private static readonly chronicleProjectsStateKey = 'g1nation.chronicleProjects';
private static readonly activeChronicleProjectStateKey = 'g1nation.activeChronicleProjectId';
private _view?: vscode.WebviewView;
public brainEnabled = true;
private _currentSessionBrainId: string | null = null;
private _currentNegativePrompt: string = '';
private readonly _chronicle = new ProjectChronicleManager();
constructor(
private readonly _extensionUri: vscode.Uri,
@@ -89,6 +93,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
await this._sendSessionList();
await this._sendModels();
await this._sendConfig();
await this._sendChronicleProjects();
await this._restoreActiveSessionIntoView();
break;
case 'toggleMultiAgent':
@@ -103,6 +108,45 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
case 'getAgents':
await this._sendAgentsList();
break;
case 'getChronicleProjects':
await this._sendChronicleProjects();
break;
case 'createChronicleProject':
await this._createChronicleProject();
break;
case 'setChronicleProject':
await this._setActiveChronicleProject(data.id);
break;
case 'openChronicleFolder':
await this._openChronicleFolder();
break;
case 'getChronicleRecords':
await this._sendChronicleRecords();
break;
case 'openChronicleRecord':
await this._openChronicleRecord(data.path);
break;
case 'writeChroniclePlanning':
await this._writeChroniclePlanningFromCurrentChat();
break;
case 'writeChronicleDiscussion':
await this._writeChronicleDiscussionFromCurrentChat();
break;
case 'writeChronicleDecision':
await this._writeChronicleDecisionFromInput();
break;
case 'writeChronicleDevelopment':
await this._writeChronicleDevelopmentFromCurrentChat();
break;
case 'writeChronicleBug':
await this._writeChronicleBugFromInput();
break;
case 'writeChronicleRetrospective':
await this._writeChronicleRetrospectiveFromInput();
break;
case 'writeChronicleRecord':
await this._writeChronicleRecord(data.recordType);
break;
case 'createAgent':
await this._createAgent();
break;
@@ -899,6 +943,568 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
});
}
private _getChronicleProjects(): ProjectProfile[] {
const raw = this._context.globalState.get<ProjectProfile[]>(SidebarChatProvider.chronicleProjectsStateKey, []) || [];
const valid = raw.filter((profile: ProjectProfile) =>
profile
&& typeof profile.projectId === 'string'
&& typeof profile.projectName === 'string'
&& typeof profile.recordRoot === 'string'
);
if (valid.length > 0) return valid;
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) return [];
const now = new Date().toISOString();
const projectName = path.basename(workspaceRoot) || 'Current Project';
return [{
projectId: this._slugify(projectName),
projectName,
projectRoot: workspaceRoot,
recordRoot: path.join(workspaceRoot, 'docs', 'records', projectName),
description: 'Auto-detected current workspace project.',
corePurpose: 'Capture project planning, decisions, development notes, bugs, and retrospectives as Markdown.',
targetUsers: ['Project developer'],
avoidDirections: ['Do not tightly couple records to chat execution internals.'],
detailLevel: 'standard',
createdAt: now,
updatedAt: now
}];
}
private async _putChronicleProjects(projects: ProjectProfile[]) {
await this._context.globalState.update(SidebarChatProvider.chronicleProjectsStateKey, projects);
}
private _getActiveChronicleProject(): ProjectProfile | null {
const projects = this._getChronicleProjects();
if (projects.length === 0) return null;
const activeId = this._context.globalState.get<string>(SidebarChatProvider.activeChronicleProjectStateKey, '');
return projects.find(project => project.projectId === activeId) || projects[0];
}
private async _sendChronicleProjects() {
if (!this._view) return;
const projects = this._getChronicleProjects();
const active = this._getActiveChronicleProject();
this._view.webview.postMessage({
type: 'chronicleProjects',
value: {
activeProjectId: active?.projectId || '',
projects: projects.map(project => ({
id: project.projectId,
name: project.projectName,
root: project.projectRoot || '',
recordRoot: project.recordRoot,
description: project.description || ''
}))
}
});
}
private async _createChronicleProject() {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
const defaultName = workspaceRoot ? path.basename(workspaceRoot) : 'New Project';
const projectName = await vscode.window.showInputBox({
prompt: 'Project name for Chronicle records',
value: defaultName,
validateInput: (value) => value.trim() ? null : 'Project name is required.'
});
if (!projectName) return;
const description = await vscode.window.showInputBox({
prompt: 'One-line project description',
value: 'Project planning, decisions, development logs, and bug records.'
});
if (description === undefined) return;
const projectRoot = await vscode.window.showInputBox({
prompt: 'Project root path',
value: workspaceRoot,
validateInput: (value) => value.trim() ? null : 'Project root is required.'
});
if (!projectRoot) return;
const defaultRecordRoot = path.join(projectRoot.trim(), 'docs', 'records', projectName.trim());
const recordRoot = await vscode.window.showInputBox({
prompt: 'Markdown record folder path',
value: defaultRecordRoot,
validateInput: (value) => value.trim() ? null : 'Record folder path is required.'
});
if (!recordRoot) return;
const corePurpose = await vscode.window.showInputBox({
prompt: 'Core project purpose or guardrail',
value: 'Keep project knowledge traceable through Markdown records.'
});
if (corePurpose === undefined) return;
const detailChoice = await vscode.window.showQuickPick([
{ label: 'standard', description: 'Request, question intent, decisions, planning, development, and bugs' },
{ label: 'simple', description: 'Request summary, decisions, and implementation result' },
{ label: 'detailed', description: 'Standard records plus alternatives, lessons, and retrospectives' }
], {
placeHolder: 'Chronicle record detail level'
});
if (!detailChoice) return;
const now = new Date().toISOString();
const projects = this._getChronicleProjects();
const idBase = this._slugify(projectName.trim());
let projectId = idBase;
let suffix = 2;
while (projects.some(project => project.projectId === projectId)) {
projectId = `${idBase}-${suffix++}`;
}
const profile: ProjectProfile = {
projectId,
projectName: projectName.trim(),
projectRoot: projectRoot.trim(),
recordRoot: recordRoot.trim(),
description: description.trim(),
corePurpose: corePurpose.trim(),
targetUsers: ['Project developer'],
avoidDirections: ['Do not mix records across projects.', 'Do not tightly couple records to core agent execution.'],
detailLevel: detailChoice.label as ProjectProfile['detailLevel'],
createdAt: now,
updatedAt: now
};
this._chronicle.ensureProject(profile);
const nextProjects = [...projects.filter(project => project.projectId !== profile.projectId), profile];
await this._putChronicleProjects(nextProjects);
await this._context.globalState.update(SidebarChatProvider.activeChronicleProjectStateKey, profile.projectId);
await this._sendChronicleProjects();
this.injectSystemMessage(`**[Designer Project Created]** ${profile.projectName}\n\`${profile.recordRoot}\``);
}
private async _setActiveChronicleProject(projectId: string) {
if (!projectId || projectId === 'new') {
await this._createChronicleProject();
return;
}
const target = this._getChronicleProjects().find(project => project.projectId === projectId);
if (!target) return;
await this._context.globalState.update(SidebarChatProvider.activeChronicleProjectStateKey, target.projectId);
await this._sendChronicleProjects();
await this._sendChronicleRecords();
this.injectSystemMessage(`**[Designer Project Selected]** ${target.projectName}\n\`${target.recordRoot}\``);
}
private async _openChronicleFolder() {
const profile = this._getActiveChronicleProject();
if (!profile) {
vscode.window.showWarningMessage('No Chronicle project is selected.');
return;
}
try {
this._chronicle.ensureProject(profile);
await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(profile.recordRoot));
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to open Chronicle folder: ${err.message}`);
}
}
private async _sendChronicleRecords() {
if (!this._view) return;
const profile = this._getActiveChronicleProject();
if (!profile) {
this._view.webview.postMessage({ type: 'chronicleRecords', value: [] });
return;
}
try {
const records = this._chronicle.listRecords(profile).map(record => ({
section: record.section,
fileName: record.fileName,
path: record.filePath,
relativePath: record.relativePath,
updatedAt: record.updatedAt
}));
this._view.webview.postMessage({ type: 'chronicleRecords', value: records });
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to list Chronicle records: ${err.message}`);
}
}
private async _openChronicleRecord(recordPath: string) {
const profile = this._getActiveChronicleProject();
if (!profile || !recordPath) {
vscode.window.showWarningMessage('Select a Chronicle record first.');
return;
}
const root = path.resolve(profile.recordRoot);
const target = path.resolve(recordPath);
if (!target.startsWith(`${root}${path.sep}`) || path.extname(target) !== '.md') {
vscode.window.showErrorMessage('Selected Chronicle record path is not valid.');
return;
}
if (!fs.existsSync(target)) {
vscode.window.showErrorMessage('Selected Chronicle record no longer exists.');
await this._sendChronicleRecords();
return;
}
const doc = await vscode.workspace.openTextDocument(target);
await vscode.window.showTextDocument(doc);
}
private async _writeChroniclePlanningFromCurrentChat() {
const profile = this._getActiveChronicleProject();
if (!profile) {
vscode.window.showWarningMessage('Select or create a Designer project first.');
return;
}
const history = this._agent.getHistory();
const latestUser = [...history].reverse().find(message => message.role === 'user')?.content || '';
const latestAssistant = [...history].reverse().find(message => message.role === 'assistant')?.content || '';
const featureName = await vscode.window.showInputBox({
prompt: 'Feature name for the planning document',
value: this._summarizeForTitle(latestUser || 'Project Chronicle Feature')
});
if (!featureName) return;
try {
const createdAt = new Date().toISOString();
const result = this._chronicle.writePlanning(profile, {
featureName: featureName.trim(),
purpose: 'Record the reason, scope, direction, and success criteria before implementation.',
background: this._summarizeTextForWiki(latestAssistant || latestUser),
userIntent: this._summarizeTextForWiki(latestUser),
sourceRequest: latestUser || 'No user request captured in the current chat.',
scope: [
'Create a project-specific planning record.',
'Capture user intent and implementation direction.',
'Keep the record independent from chat execution internals.'
],
outOfScope: [
'Full automatic transcript capture.',
'External database integration.',
'Git automation.'
],
developmentDirection: 'Use Project Chronicle as a low-dependency Markdown record layer.',
dependencyStrategy: 'Use local filesystem writes through the independent projectChronicle module.',
expectedValue: 'Future work can understand why this feature exists and what decisions shaped it.',
successCriteria: [
'The planning document is created under the selected project record folder.',
'The document includes user intent, scope, out-of-scope items, and success criteria.'
],
developerInstruction: 'Use this document as the implementation guardrail for the next development step.',
createdAt
});
this._chronicle.appendTimeline(profile, [`Planning record created: ${result.relativePath}`], createdAt);
vscode.window.showInformationMessage(`Chronicle planning saved: ${result.relativePath}`);
await this._sendChronicleRecords();
this.injectSystemMessage(`**[Chronicle Planning Saved]** \`${result.filePath}\``);
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to write planning record: ${err.message}`);
}
}
private async _writeChronicleDiscussionFromCurrentChat() {
const profile = this._getActiveChronicleProject();
if (!profile) {
vscode.window.showWarningMessage('Select or create a Designer project first.');
return;
}
const history = this._agent.getHistory();
const latestUser = [...history].reverse().find(message => message.role === 'user')?.content || '';
const latestAssistant = [...history].reverse().find(message => message.role === 'assistant')?.content || '';
const title = await vscode.window.showInputBox({
prompt: 'Discussion title',
value: this._summarizeForTitle(latestUser || 'Project Discussion')
});
if (!title) return;
const question = await vscode.window.showInputBox({
prompt: 'AI question to record (optional)',
value: ''
});
if (question === undefined) return;
let questions: any[] = [];
if (question.trim()) {
const reason = await vscode.window.showInputBox({
prompt: 'Why was this question asked?',
value: 'To avoid writing records to the wrong project or making an unclear design decision.'
});
if (reason === undefined) return;
const impact = await vscode.window.showInputBox({
prompt: 'How does this question affect the decision?',
value: 'It determines the correct project context, scope, or implementation path.'
});
if (impact === undefined) return;
questions = [{
question: question.trim(),
reason: reason.trim(),
expectedInformation: 'Information needed to clarify project context, scope, or decision direction.',
impactOnDecision: impact.trim()
}];
}
try {
const createdAt = new Date().toISOString();
const result = this._chronicle.writeDiscussion(profile, {
title: title.trim(),
userRequest: this._summarizeTextForWiki(latestUser || 'No user request captured in the current chat.'),
interpretedIntent: 'Capture the discussion as a reusable project knowledge record instead of leaving it only in chat history.',
questions,
discussions: [
this._summarizeTextForWiki(latestAssistant || latestUser || 'No discussion detail captured yet.')
],
decisions: [],
createdAt
});
this._chronicle.appendTimeline(profile, [`Discussion record created: ${result.relativePath}`], createdAt);
vscode.window.showInformationMessage(`Chronicle discussion saved: ${result.relativePath}`);
await this._sendChronicleRecords();
this.injectSystemMessage(`**[Chronicle Discussion Saved]** \`${result.filePath}\``);
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to write discussion record: ${err.message}`);
}
}
private async _writeChronicleDecisionFromInput() {
const profile = this._getActiveChronicleProject();
if (!profile) {
vscode.window.showWarningMessage('Select or create a Designer project first.');
return;
}
const title = await vscode.window.showInputBox({
prompt: 'Decision title',
value: 'Use independent Markdown record module'
});
if (!title) return;
const decision = await vscode.window.showInputBox({
prompt: 'Decision',
value: 'Implement this behavior as an independent Project Chronicle module.'
});
if (decision === undefined) return;
const reason = await vscode.window.showInputBox({
prompt: 'Decision reason',
value: 'To reduce coupling and keep project records portable.'
});
if (reason === undefined) return;
const alternatives = await vscode.window.showInputBox({
prompt: 'Rejected alternatives (comma-separated)',
value: 'Integrate with Second Brain, integrate directly into Agent execution'
});
if (alternatives === undefined) return;
try {
const createdAt = new Date().toISOString();
const adrNumber = this._chronicle.nextAdrNumber(profile);
const result = this._chronicle.writeDecision(profile, {
title: title.trim(),
status: 'accepted',
context: 'A project record needs to capture not only what changed, but why the direction was chosen.',
decision: decision.trim(),
reason: reason.trim(),
alternatives: alternatives.split(',').map(item => item.trim()).filter(Boolean),
consequences: [
'Records can evolve independently from chat and agent internals.',
'Future automation can emit chronicle events without owning the core execution path.'
],
createdAt
}, adrNumber);
this._chronicle.appendTimeline(profile, [`Decision record created: ${result.relativePath}`], createdAt);
vscode.window.showInformationMessage(`Chronicle decision saved: ${result.relativePath}`);
await this._sendChronicleRecords();
this.injectSystemMessage(`**[Chronicle Decision Saved]** \`${result.filePath}\``);
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to write decision record: ${err.message}`);
}
}
private async _writeChronicleDevelopmentFromCurrentChat() {
const profile = this._getActiveChronicleProject();
if (!profile) {
vscode.window.showWarningMessage('Select or create a Designer project first.');
return;
}
const history = this._agent.getHistory();
const latestUser = [...history].reverse().find(message => message.role === 'user')?.content || '';
const latestAssistant = [...history].reverse().find(message => message.role === 'assistant')?.content || '';
const featureName = await vscode.window.showInputBox({
prompt: 'Feature name for the development log',
value: this._summarizeForTitle(latestUser || 'Implementation Log')
});
if (!featureName) return;
try {
const createdAt = new Date().toISOString();
const result = this._chronicle.writeDevelopmentLog(profile, {
featureName: featureName.trim(),
purpose: 'Record the actual implementation outcome for later maintenance.',
implementationSummary: this._summarizeTextForWiki(latestAssistant || 'Implementation summary was not captured from chat yet.'),
architecture: 'Project Chronicle records are written through an independent Markdown module.',
changedFiles: ['Capture exact changed files after verification.'],
dependencyNotes: 'Keep dependencies limited to TypeScript, Node fs/path, and VS Code extension APIs.',
bugs: [],
lessons: [
'Write implementation notes as soon as a stable development step finishes.',
'Keep generated records project-specific.'
],
createdAt
});
this._chronicle.appendTimeline(profile, [`Development log created: ${result.relativePath}`], createdAt);
vscode.window.showInformationMessage(`Chronicle development log saved: ${result.relativePath}`);
await this._sendChronicleRecords();
this.injectSystemMessage(`**[Chronicle Development Saved]** \`${result.filePath}\``);
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to write development record: ${err.message}`);
}
}
private async _writeChronicleBugFromInput() {
const profile = this._getActiveChronicleProject();
if (!profile) {
vscode.window.showWarningMessage('Select or create a Designer project first.');
return;
}
const title = await vscode.window.showInputBox({
prompt: 'Bug title',
value: 'record-generation-issue'
});
if (!title) return;
const symptom = await vscode.window.showInputBox({
prompt: 'Bug symptom',
value: 'Describe what failed or looked wrong.'
});
if (symptom === undefined) return;
const cause = await vscode.window.showInputBox({
prompt: 'Bug cause',
value: 'Cause is not confirmed yet.'
});
if (cause === undefined) return;
const fix = await vscode.window.showInputBox({
prompt: 'Fix',
value: 'Describe the fix or mitigation.'
});
if (fix === undefined) return;
try {
const createdAt = new Date().toISOString();
const bugNumber = this._chronicle.nextBugNumber(profile);
const result = this._chronicle.writeBug(profile, {
title: title.trim(),
symptom: symptom.trim(),
cause: cause.trim(),
fix: fix.trim(),
prevention: 'Validate project selection, record path, and write permissions before generating files.',
createdAt
}, bugNumber);
this._chronicle.appendTimeline(profile, [`Bug record created: ${result.relativePath}`], createdAt);
vscode.window.showInformationMessage(`Chronicle bug record saved: ${result.relativePath}`);
await this._sendChronicleRecords();
this.injectSystemMessage(`**[Chronicle Bug Saved]** \`${result.filePath}\``);
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to write bug record: ${err.message}`);
}
}
private async _writeChronicleRetrospectiveFromInput() {
const profile = this._getActiveChronicleProject();
if (!profile) {
vscode.window.showWarningMessage('Select or create a Designer project first.');
return;
}
const title = await vscode.window.showInputBox({
prompt: 'Retrospective title',
value: 'Project Chronicle Guard iteration'
});
if (!title) return;
const summary = await vscode.window.showInputBox({
prompt: 'Work summary',
value: 'Completed an incremental development step and recorded the outcome.'
});
if (summary === undefined) return;
const wentWell = await vscode.window.showInputBox({
prompt: 'What went well? (comma-separated)',
value: 'Kept the feature independent, Generated Markdown records, Preserved project context'
});
if (wentWell === undefined) return;
const toImprove = await vscode.window.showInputBox({
prompt: 'What should improve? (comma-separated)',
value: 'More automatic question intent capture, Richer record editing UI'
});
if (toImprove === undefined) return;
const nextActions = await vscode.window.showInputBox({
prompt: 'Next actions (comma-separated)',
value: 'Add tests, Improve Designer UI, Add event-based record capture'
});
if (nextActions === undefined) return;
try {
const createdAt = new Date().toISOString();
const result = this._chronicle.writeRetrospective(profile, {
title: title.trim(),
summary: summary.trim(),
wentWell: wentWell.split(',').map(item => item.trim()).filter(Boolean),
toImprove: toImprove.split(',').map(item => item.trim()).filter(Boolean),
nextActions: nextActions.split(',').map(item => item.trim()).filter(Boolean),
createdAt
});
this._chronicle.appendTimeline(profile, [`Retrospective created: ${result.relativePath}`], createdAt);
vscode.window.showInformationMessage(`Chronicle retrospective saved: ${result.relativePath}`);
await this._sendChronicleRecords();
this.injectSystemMessage(`**[Chronicle Retrospective Saved]** \`${result.filePath}\``);
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to write retrospective record: ${err.message}`);
}
}
private async _writeChronicleRecord(recordType: string) {
switch (recordType) {
case 'planning':
await this._writeChroniclePlanningFromCurrentChat();
break;
case 'discussion':
await this._writeChronicleDiscussionFromCurrentChat();
break;
case 'decision':
await this._writeChronicleDecisionFromInput();
break;
case 'development':
await this._writeChronicleDevelopmentFromCurrentChat();
break;
case 'bug':
await this._writeChronicleBugFromInput();
break;
case 'retrospective':
await this._writeChronicleRetrospectiveFromInput();
break;
default:
vscode.window.showWarningMessage('Select a Chronicle record type first.');
}
}
private _getAgentsDir(): string {
const defaultPath = 'E:\\Wiki\\Agent\\.agent\\skills';
if (fs.existsSync(defaultPath)) return defaultPath;
@@ -1043,7 +1649,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
private async _handlePrompt(data: any) {
if (!this._view) return;
const { value, model, internet, files, agentFile, negativePrompt } = data;
const { value, model, internet, files, agentFile, negativePrompt, designerGuard, secondBrainTrace, secondBrainTraceDebug } = data;
this._currentNegativePrompt = negativePrompt || '';
this._currentSessionBrainId = getActiveBrainProfile().id;
@@ -1052,12 +1658,17 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
agentSkillContext = fs.readFileSync(agentFile, 'utf8');
}
const designerContext = designerGuard !== false ? this._buildDesignerGuardContext() : undefined;
try {
await this._agent.handlePrompt(value, model, {
internetEnabled: internet,
visionContent: files,
agentSkillContext,
negativePrompt
negativePrompt,
designerContext,
secondBrainTraceEnabled: secondBrainTrace !== false,
secondBrainTraceDebug: !!secondBrainTraceDebug
});
} catch (error: any) {
logError('Prompt handling failed in sidebar provider.', { error: error?.message || String(error), promptPreview: summarizeText(value || '', 200) });
@@ -1065,6 +1676,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
}
private _buildDesignerGuardContext(): string {
return buildProjectChronicleGuardContext(this._getActiveChronicleProject());
}
private async _sendModels() {
if (!this._view) return;
try {
@@ -1755,6 +2370,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
<div class="header-actions">
<button class="icon-btn" id="newChatBtn" data-tooltip="New Chat">New</button>
<button class="icon-btn" id="saveWikiRawBtn" data-tooltip="Save Wiki Raw">Wiki</button>
<button class="icon-btn active" id="designerGuardBtn" data-tooltip="Chronicle Guard Mode: Auto">Guard</button>
<button class="icon-btn active" id="brainTraceBtn" data-tooltip="Second Brain Trace Mode">Trace</button>
<button class="icon-btn" id="brainTraceDebugBtn" data-tooltip="Second Brain Debug JSON">Dbg</button>
<button class="icon-btn" id="multiAgentBtn" data-tooltip="Multi-Agent Mode">MA</button>
<button class="icon-btn" id="internetBtn" data-tooltip="Internet Access">Web</button>
<button class="icon-btn" id="historyBtn" data-tooltip="View History">Log</button>
@@ -1786,6 +2404,35 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
</div>
</div>
</div>
<div class="control-row">
<div class="select-wrap"><select id="designerSel" title="Select Designer Project"></select></div>
<div class="tool-group" aria-label="Designer actions">
<button class="icon-btn" id="addDesignerBtn" data-tooltip="Create Designer Project">Add</button>
<button class="icon-btn" id="openDesignerBtn" data-tooltip="Open Record Folder">Open</button>
</div>
</div>
<div class="control-row">
<div class="select-wrap">
<select id="chronicleRecordTypeSel" title="Select Chronicle Record Type">
<option value="planning">Planning</option>
<option value="discussion">Discussion</option>
<option value="decision">Decision</option>
<option value="development">Development</option>
<option value="bug">Bug</option>
<option value="retrospective">Retrospective</option>
</select>
</div>
<div class="tool-group" aria-label="Chronicle write actions">
<button class="icon-btn" id="writeChronicleBtn" data-tooltip="Write Selected Record">Write</button>
</div>
</div>
<div class="control-row">
<div class="select-wrap"><select id="chronicleRecordSel" title="Select Chronicle Record"></select></div>
<div class="tool-group" aria-label="Chronicle record actions">
<button class="icon-btn" id="refreshChronicleRecordsBtn" data-tooltip="Refresh Records">Ref</button>
<button class="icon-btn" id="openChronicleRecordBtn" data-tooltip="Open Selected Record">Open</button>
</div>
</div>
</div>
</div>
</div>
@@ -1864,7 +2511,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
function saveWebviewState(history) {
vscode.setState({ history });
const current = vscode.getState() || {};
vscode.setState({ ...current, history });
}
function saveUiState() {
const current = vscode.getState() || {};
vscode.setState({ ...current, designerGuardEnabled, secondBrainTraceEnabled, secondBrainTraceDebug });
}
function renderHistory(history) {
@@ -1989,6 +2642,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const fileInput = document.getElementById('fileInput');
const attachPreview = document.getElementById('attachPreview');
const agentSel = document.getElementById('agentSel');
const designerSel = document.getElementById('designerSel');
const chronicleRecordTypeSel = document.getElementById('chronicleRecordTypeSel');
const chronicleRecordSel = document.getElementById('chronicleRecordSel');
const editAgentBtn = document.getElementById('editAgentBtn');
const addAgentBtn = document.getElementById('addAgentBtn');
const deleteAgentBtn = document.getElementById('deleteAgentBtn');
@@ -2003,8 +2659,29 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
let streamBody = null;
let internetEnabled = false;
let designerGuardEnabled = true;
let secondBrainTraceEnabled = true;
let secondBrainTraceDebug = false;
let pendingFiles = [];
let editMode = false;
if (previousState && typeof previousState.designerGuardEnabled === 'boolean') {
designerGuardEnabled = previousState.designerGuardEnabled;
}
if (previousState && typeof previousState.secondBrainTraceEnabled === 'boolean') {
secondBrainTraceEnabled = previousState.secondBrainTraceEnabled;
}
if (previousState && typeof previousState.secondBrainTraceDebug === 'boolean') {
secondBrainTraceDebug = previousState.secondBrainTraceDebug;
}
const initialGuardBtn = document.getElementById('designerGuardBtn');
initialGuardBtn.classList.toggle('active', designerGuardEnabled);
initialGuardBtn.setAttribute('data-tooltip', designerGuardEnabled ? 'Chronicle Guard Mode: On' : 'Chronicle Guard Mode: Off');
const initialTraceBtn = document.getElementById('brainTraceBtn');
initialTraceBtn.classList.toggle('active', secondBrainTraceEnabled);
initialTraceBtn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
const initialTraceDebugBtn = document.getElementById('brainTraceDebugBtn');
initialTraceDebugBtn.classList.toggle('active', secondBrainTraceDebug);
initialTraceDebugBtn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
function fmt(text) { return marked.parse(text || ''); }
@@ -2211,6 +2888,39 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
vscode.postMessage({ type: 'getAgentContent', path: msg.selected });
}
break;
case 'chronicleProjects':
designerSel.innerHTML = '';
msg.value.projects.forEach(p => {
const o = document.createElement('option');
o.value = p.id;
o.innerText = p.name;
o.title = p.recordRoot;
if (p.id === msg.value.activeProjectId) o.selected = true;
designerSel.appendChild(o);
});
const newDesignerOpt = document.createElement('option');
newDesignerOpt.value = 'new';
newDesignerOpt.innerText = '+ Add Designer Project...';
designerSel.appendChild(newDesignerOpt);
vscode.postMessage({ type: 'getChronicleRecords' });
break;
case 'chronicleRecords':
chronicleRecordSel.innerHTML = '';
if (!msg.value || msg.value.length === 0) {
const emptyRecordOpt = document.createElement('option');
emptyRecordOpt.value = '';
emptyRecordOpt.innerText = 'No records yet';
chronicleRecordSel.appendChild(emptyRecordOpt);
break;
}
msg.value.forEach(record => {
const o = document.createElement('option');
o.value = record.path;
o.innerText = record.relativePath;
o.title = record.path;
chronicleRecordSel.appendChild(o);
});
break;
case 'agentContent':
agentPrompt.value = msg.value;
negativePrompt.value = msg.negativePrompt || '';
@@ -2324,7 +3034,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
internet: internetEnabled,
files: pendingFiles.length > 0 ? pendingFiles : undefined,
agentFile: agentSel.value === 'none' ? undefined : agentSel.value,
negativePrompt: negativePrompt.value.trim() || undefined
negativePrompt: negativePrompt.value.trim() || undefined,
designerGuard: designerGuardEnabled,
secondBrainTrace: secondBrainTraceEnabled,
secondBrainTraceDebug
});
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
// 전송 완료 후 Draft State 리셋 + Stop 버튼 표시
@@ -2370,6 +3083,27 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
document.getElementById('internetBtn').onclick = () => {
internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled);
};
document.getElementById('designerGuardBtn').onclick = () => {
designerGuardEnabled = !designerGuardEnabled;
const btn = document.getElementById('designerGuardBtn');
btn.classList.toggle('active', designerGuardEnabled);
btn.setAttribute('data-tooltip', designerGuardEnabled ? 'Chronicle Guard Mode: On' : 'Chronicle Guard Mode: Off');
saveUiState();
};
document.getElementById('brainTraceBtn').onclick = () => {
secondBrainTraceEnabled = !secondBrainTraceEnabled;
const btn = document.getElementById('brainTraceBtn');
btn.classList.toggle('active', secondBrainTraceEnabled);
btn.setAttribute('data-tooltip', secondBrainTraceEnabled ? 'Second Brain Trace Mode: On' : 'Second Brain Trace Mode: Off');
saveUiState();
};
document.getElementById('brainTraceDebugBtn').onclick = () => {
secondBrainTraceDebug = !secondBrainTraceDebug;
const btn = document.getElementById('brainTraceDebugBtn');
btn.classList.toggle('active', secondBrainTraceDebug);
btn.setAttribute('data-tooltip', secondBrainTraceDebug ? 'Second Brain Debug JSON: On' : 'Second Brain Debug JSON: Off');
saveUiState();
};
let multiAgentEnabled = false;
const setMultiAgentUi = (enabled) => {
@@ -2427,6 +3161,15 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
};
designerSel.onchange = () => {
if (designerSel.value === 'new') {
vscode.postMessage({ type: 'createChronicleProject' });
} else {
vscode.postMessage({ type: 'setChronicleProject', id: designerSel.value });
vscode.postMessage({ type: 'getChronicleRecords' });
}
};
// Handle initial state and state updates from extension
window.addEventListener('message', e => {
const msg = e.data;
@@ -2473,8 +3216,22 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
vscode.postMessage({ type: 'deleteAgent', path: agentSel.value });
};
document.getElementById('addDesignerBtn').onclick = () => vscode.postMessage({ type: 'createChronicleProject' });
document.getElementById('openDesignerBtn').onclick = () => vscode.postMessage({ type: 'openChronicleFolder' });
document.getElementById('writeChronicleBtn').onclick = () => vscode.postMessage({
type: 'writeChronicleRecord',
recordType: chronicleRecordTypeSel.value
});
document.getElementById('refreshChronicleRecordsBtn').onclick = () => vscode.postMessage({ type: 'getChronicleRecords' });
document.getElementById('openChronicleRecordBtn').onclick = () => {
if (!chronicleRecordSel.value) return;
vscode.postMessage({ type: 'openChronicleRecord', path: chronicleRecordSel.value });
};
vscode.postMessage({ type: 'getModels' });
vscode.postMessage({ type: 'getAgents' });
vscode.postMessage({ type: 'getChronicleProjects' });
vscode.postMessage({ type: 'getChronicleRecords' });
vscode.postMessage({ type: 'ready' });
// --- Proactive Behavioral Tracking ---
+2
View File
@@ -150,6 +150,8 @@ Core behavior:
- Do not output hidden reasoning labels such as [PROBLEM], [GOAL], [REASONING], Phase 0, Fidelity Lock-in, or process manifestos.
- For substantial answers, write readable Markdown using ## and ### headings, short paragraphs, bullets, and tables where useful.
- Avoid wall-of-text output. Make the answer scannable before adding detail.
- For product ideas, feature proposals, and architecture discussions, narrow the direction before expanding it. Prefer a practical MVP first, then separate later expansion ideas.
- Avoid inflated consulting language. Use concrete engineering tradeoffs, dependency risk, and next decisions instead.
Available action tags: