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
+18
View File
@@ -1,3 +1,21 @@
# Patch Notes - v2.35.0 (2026-05-02)
## 🏛️ Milestone: Knowledge Resilience & Standardization
- **Authoritative Knowledge Base:** Completed the full synthesis of all raw engineering artifacts into the `10_Wiki` system, establishing a single source of truth for Cognitive Engineering, Agentic Coding, and Security Governance.
- **Zero-Ghost Integrity:** Finalized global graph cleanup, ensuring the Obsidian brain remains 100% free of broken nodes and fragmented stubs.
- **VSIX Distribution:** Optimized packaging for the new standardized environment, ensuring seamless integration with the latest knowledge clusters.
---
# Patch Notes - v2.34.1 (2026-05-02)
## 🧠 Knowledge Base Standardized (P-Reinforce v3.0)
- **High-Density Wikification:** Synthesized 148+ raw cognitive engineering and software maintenance artifacts into 9 authoritative, cross-linked clusters.
- **Graph Integrity Purge:** Executed global regex-based cleanup to eliminate all broken links (Ghost Nodes) and empty stubs, ensuring 100% structural reliability.
- **Topic Architecture:** Reorganized knowledge into standardized categories (Cognitive Engineering, AI/Agentic Coding, Security/Quality, etc.) for optimized discovery.
---
# Patch Notes - v2.33.9 (2026-05-01)
## 🚀 Core Engine Upgrade
+18
View File
@@ -0,0 +1,18 @@
# ConnectAI Project Chronicle Records
This folder stores planning, decision, development, bug, and retrospective records for ConnectAI.
## Project
- Name: ConnectAI
- Root: `/Volumes/Data/project/Antigravity/ConnectAI`
- Record root: `docs/records/ConnectAI`
## Structure
- `project-profile.md`: project identity and guardrails
- `timeline.md`: high-level chronological history
- `planning/`: feature planning records
- `discussions/`: AI-human discussion summaries
- `decisions/`: decision records
- `development/`: implementation logs
- `bugs/`: bug and fix records
- `retrospectives/`: post-work reviews
@@ -0,0 +1,27 @@
# ADR-0001: Implement Project Chronicle Guard As An Independent Module
## Status
Accepted
## Context
The requested feature records project planning, questions, decisions, development logs, bugs, and retrospectives. Existing chat and agent systems already manage model interaction and agent skills.
## Decision
Implement Project Chronicle Guard as a separate module under `src/features/projectChronicle`.
## Reason
- It reduces the chance of regressions in chat and agent execution.
- It keeps the MVP focused on local Markdown generation.
- It can later receive events from chat or agents without owning those flows.
- It makes project-specific record storage easier to test and evolve.
## Alternatives
- Integrate into the existing Second Brain flow.
- Extend Agent Skill files to double as project records.
- Add a standalone Project Chronicle module.
## Selected Alternative
Add a standalone Project Chronicle module.
## Consequences
The first stage needs explicit sidebar actions to create and write records. Automatic extraction can be layered on later.
@@ -0,0 +1,39 @@
# Development Log: Project Chronicle Guard Feedback Response
## Purpose
Improve Chronicle Guard behavior after testing showed the assistant still answered like a general idea proposer instead of a project planning and record guard.
## Feedback Summary
The tested response understood the broad idea, but missed key guard behaviors:
- Project target check
- Record path check
- Question intent and question reasons
- Pending decision candidates
- Low-dependency MVP first
- Candidate records at the end
- Plain engineering tone
## Implementation Summary
- Made Chronicle Guard context apply by default unless explicitly disabled from the webview payload.
- Strengthened the Guard response contract for new ideas and feature requests.
- Added required response order: summary, intent, project check, record path check, blocking questions, question reasons, direction review, MVP, later expansion, candidate records.
- Added decision policy so unconfirmed decisions stay pending.
- Added tone rules against inflated consulting language and premature Vector DB / relational DB / knowledge graph suggestions.
- Extracted Guard prompt generation into `buildProjectChronicleGuardContext`.
- Added tests that lock the most important Guard rules.
- Added base system prompt guidance to prefer practical MVP-first answers for product and architecture discussions.
## Changed Files
- `src/features/projectChronicle/guardPrompt.ts`
- `src/features/projectChronicle/index.ts`
- `src/sidebarProvider.ts`
- `src/utils.ts`
- `tests/projectChronicleGuardPrompt.test.ts`
## Verification
- `./node_modules/.bin/tsc --noEmit`
- `npm run compile`
- `./node_modules/.bin/jest --runInBand`
## Result
Chronicle Guard should now behave less like a general ideation assistant and more like a project planning, decision, and record guard.
@@ -0,0 +1,48 @@
# Development Log: Project Chronicle Guard Stage 1
## Purpose
Add the first stable stage of the Designer menu and Project Chronicle Guard without coupling it tightly to chat execution.
## Implementation Summary
- Added an independent `src/features/projectChronicle` module.
- Added typed project, planning, discussion, decision, development, bug, and retrospective records.
- Added a Markdown writer with folder creation and filename collision protection.
- Added templates for project seed files and record documents.
- Added a Designer row in the sidebar with project selection and explicit record actions.
- Added VS Code-side handlers for creating/selecting record projects and writing planning, development, and bug records.
## Architecture
```text
Sidebar Designer UI
-> Webview postMessage
-> SidebarChatProvider handlers
-> ProjectChronicleManager
-> MarkdownFileWriter
-> docs/records/{Project}/...
```
## Changed Files
- `src/features/projectChronicle/types.ts`
- `src/features/projectChronicle/markdownFileWriter.ts`
- `src/features/projectChronicle/templates.ts`
- `src/features/projectChronicle/index.ts`
- `src/sidebarProvider.ts`
- `docs/records/ConnectAI/...`
## Dependency Notes
The implementation uses only TypeScript, Node `fs`/`path`, and VS Code extension APIs already available in the project.
## Verification
- `npm run compile`
- `./node_modules/.bin/jest --runInBand`
## Known Limits
- The records are generated through explicit sidebar actions.
- Full automatic conversation analysis is not implemented in this stage.
- Retrospective and ADR writing are available in the module, but the sidebar only exposes Plan, Dev, and Bug actions in this first pass.
## Next Development Notes
- Add a richer Designer panel for ADR, discussion, and retrospective creation.
- Capture AI question intent more directly during the conversation flow.
- Add tests for the Project Chronicle manager.
@@ -0,0 +1,47 @@
# Development Log: Project Chronicle Guard Stage 2
## Purpose
Expand the Designer menu from basic Plan/Dev/Bug writing into a fuller MVP record writer.
## Implementation Summary
- Reworked the Designer sidebar controls into a project selector plus a record type selector.
- Added explicit write flows for discussion, decision, and retrospective records.
- Preserved the existing planning, development, and bug writers.
- Added question intent capture in the discussion writer through optional prompts.
- Added ADR numbering for decision records and timeline updates for every record type.
- Expanded tests to verify all major record categories are written to their expected folders.
## Design Adjustment
The first pass exposed separate buttons for Plan, Dev, and Bug. Stage 2 changes this to a more scalable pattern:
```text
Designer Project Selector
Record Type Selector
Write / Open / Add actions
```
This keeps the sidebar usable as more record types are added.
## Changed Files
- `src/sidebarProvider.ts`
- `tests/projectChronicle.test.ts`
- `docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-2_implementation.md`
## Verification
- `./node_modules/.bin/tsc --noEmit`
- `npm run compile`
- `./node_modules/.bin/jest --runInBand`
## Result
The Designer menu can now create the MVP record types:
- Planning
- Discussion
- Decision
- Development
- Bug
- Retrospective
## Next Development Notes
- Add a dedicated Designer panel for editing richer record content before writing.
- Add automatic conversation event capture so records can be generated with less manual input.
- Add settings for default record detail level and automatic timeline behavior.
@@ -0,0 +1,42 @@
# Development Log: Project Chronicle Guard Stages 3 to 5
## Purpose
Complete the remaining MVP support around Designer mode, record browsing, and project configuration persistence.
## Stage 3: Chronicle Guard Mode
- Added a `Guard` toggle in the sidebar header.
- When enabled, active Designer project context is injected into the agent system context.
- The guard context asks the assistant to summarize requests, infer intent, explain blocking questions, identify decisions, and name records that should be written.
- The mode is restored through webview state.
## Stage 4: Record Browser
- Added a record list selector for generated Chronicle Markdown files.
- Added refresh and open actions for selected records.
- Added record listing to `ProjectChronicleManager`.
- Added path validation before opening selected Markdown records.
## Stage 5: Project Config File
- Added `chronicle.config.json` generation under each project record root.
- The config file stores project id, project name, root, record root, description, purpose, detail level, and timestamps.
- This gives the record folder its own portable project metadata, independent of VS Code global state.
## Changed Files
- `src/agent.ts`
- `src/sidebarProvider.ts`
- `src/features/projectChronicle/index.ts`
- `src/features/projectChronicle/types.ts`
- `tests/projectChronicle.test.ts`
## Verification
- `./node_modules/.bin/tsc --noEmit`
- `npm run compile`
- `./node_modules/.bin/jest --runInBand`
## Result
The staged MVP is now functionally complete:
- Select or create Designer project
- Generate project record structure
- Persist project config
- Enable Chronicle Guard response mode
- Write all MVP record types
- Browse and open generated Markdown records
@@ -0,0 +1,35 @@
# Development Log: Second Brain Trace Mode
## Purpose
Make Second Brain usage visible and testable in AI answers.
## Implementation Summary
- Added `src/features/secondBrainTrace.ts`.
- Added trace scoring over active Second Brain Markdown files.
- Added user-facing trace Markdown with usage status, referenced documents, unused documents, and grounding metrics.
- Added debug JSON output when debug mode is enabled.
- Added Trace and Dbg sidebar buttons.
- Connected trace context into `AgentExecutor`.
- Added tests for retrieval, rendering, debug JSON, and no-use cases.
## Changed Files
- `src/features/secondBrainTrace.ts`
- `src/agent.ts`
- `src/sidebarProvider.ts`
- `tests/secondBrainTrace.test.ts`
- `docs/records/ConnectAI/planning/2026-05-02_second-brain-trace-mode.md`
- `docs/records/ConnectAI/development/2026-05-02_second-brain-trace-mode_implementation.md`
## Verification
- `./node_modules/.bin/tsc --noEmit`
- `npm run compile`
- `./node_modules/.bin/jest --runInBand`
## Result
When Trace mode is on, answers can now include:
- 2nd Brain usage status
- Reason for use or non-use
- Referenced note paths and excerpts
- Searched but unused notes
- Retrieval and grounding metrics
- Optional debug JSON
@@ -0,0 +1,27 @@
# AI-Human Discussion Log
## Topic
Project Chronicle Guard sidebar Designer menu and Markdown record system.
## User Request Summary
The user wants a Designer menu in the sidebar and a staged implementation of a project planning, decision, and development record system.
## AI Questions
### Question
No blocking question was asked.
### Reason
The current workspace, project root, and a reasonable record location were available from local context.
### Impact On Decision
The first implementation can proceed with `ConnectAI` as the active project and `docs/records/ConnectAI` as the planning record location.
## Main Discussion
The feature should not replace code generation or existing agent execution. It should sit beside those features as a recording and decision support layer.
## Decisions
- Use an independent `src/features/projectChronicle` module.
- Add a sidebar Designer control similar to existing Brain and Agent controls.
- Keep the MVP file-based and Markdown-only.
- Do not implement full automatic transcript capture in the first stage.
@@ -0,0 +1,58 @@
# Feature Plan: Project Chronicle Guard
## 1. Feature Name
Project Chronicle Guard
## 2. Reason
The user wants AI-human planning, questions, decisions, implementation details, bugs, and retrospectives to remain as project-specific Markdown knowledge instead of disappearing into chat history.
## 3. Original User Request
Add a Designer menu to the sidebar, similar in spirit to Team, and implement a staged Project Chronicle Guard system that records planning, decisions, development logs, bug fixes, and retrospectives.
## 4. Interpreted Intent
The user does not only want another chat prompt preset. They want a durable project knowledge layer that captures why decisions were made and keeps records separated by project.
## 5. Problems To Solve
- Later readers forget why a direction was chosen.
- AI questions lose their intent and decision impact.
- Planning and implementation history are mixed or absent.
- Multiple projects can contaminate each other's records.
## 6. MVP Scope
- Add an independent `projectChronicle` module.
- Add project profile types and Markdown writer utilities.
- Add sidebar Designer controls for record project selection and basic project management.
- Generate the project folder structure and seed Markdown files.
- Save a planning document, discussion summary, decision record, development log, bug log, and timeline entry through explicit user actions.
## 7. Out Of Scope
- Full automatic transcript recording
- External database storage
- Git auto-commit
- Real-time file watching
- Search UI
- Tight integration with agent execution internals
## 8. Development Direction
Start with a low-risk independent module and connect the sidebar through message events. The chat agent can remain unaware of the record module in the first stage.
## 9. Existing Feature Relationship
The current sidebar already has Brain and Agent controls. The Designer menu should follow that UI pattern but manage project record profiles instead of agent skill files.
## 10. Dependency Strategy
Use TypeScript, Node `fs`, and Markdown files only. Store project profile metadata in VS Code global state and avoid modifying core chat execution.
## 11. Expected Value
- Project-specific planning and decision history becomes reusable.
- The user can intentionally create records before or after development work.
- Future automation can build on a stable local file structure.
## 12. Success Criteria
- A Designer selector appears in the sidebar.
- A new record project can be created.
- Record folders and seed files are generated safely.
- Markdown documents can be generated without needing external services.
- Existing chat, brain, and agent flows continue to compile.
## 13. Developer Instruction
Implement the first stage only: independent module, basic Designer menu, project creation/selection, folder seeding, and explicit sample record generation actions. Keep automatic conversation analysis for a later stage.
@@ -0,0 +1,36 @@
# Feature Plan: Second Brain Trace Mode
## 1. Feature Name
Second Brain Trace Mode
## 2. Reason
The user wants to verify whether AI answers actually use Second Brain notes, which documents were searched, and how retrieved knowledge influenced the answer.
## 3. Original User Request
Add a way to see if the AI searched Second Brain, which notes it referenced, whether each note was used in the final answer, and why unused notes were excluded.
## 4. Interpreted User Intent
The user does not want to rely on vague claims that the AI used memory. They want visible grounding evidence and a developer debug trace.
## 5. Scope
- Add a trace mode toggle in the sidebar.
- Add a debug JSON toggle.
- Retrieve relevant Markdown notes from the active Second Brain.
- Inject selected notes into the answer context.
- Append a user-visible 2nd Brain usage section to answers.
- Show searched-but-unused notes and basic grounding metrics.
## 6. Out Of Scope
- Vector database integration
- Full semantic embedding search
- Perfect factual attribution at sentence level
- External observability tools
## 7. Development Direction
Use a low-dependency local Markdown search first. Keep vector search and richer grounding analysis as later expansion.
## 8. Success Criteria
- Users can see whether Second Brain was used.
- Users can see referenced Markdown paths.
- Debug mode exposes retrieval JSON.
- General questions can explicitly show that Second Brain was not used.
+30
View File
@@ -0,0 +1,30 @@
# Project Profile
## Project Name
ConnectAI
## Purpose
Provide a local AI assistant experience inside VS Code with chat, local knowledge, agent skills, and project-oriented development support.
## Core Users
- Project developer
- Local AI workflow user
- Developer who wants project knowledge to persist as Markdown
## Core Direction
- Keep new support features independent from the main chat and agent execution paths.
- Prefer Markdown and local filesystem storage for project knowledge.
- Avoid external databases for the MVP.
- Keep user decisions, question intent, implementation notes, and bug fixes traceable.
## Avoid
- Strong coupling between record generation and agent internals
- Automatic file writes without clear user action or selected record project
- Mixing records from different projects
- Complex long-term memory systems in the MVP
## Priority
1. Stability
2. Clear records
3. Low dependency
4. Incremental delivery
+13
View File
@@ -0,0 +1,13 @@
# Project Timeline
## 2026-05-02
- Discussed adding a sidebar Designer menu for a Project Chronicle Guard system.
- Decided the feature should behave like an independent record layer, similar in spirit to the existing agent/team-style selector.
- Chose an incremental MVP path: first add project record management and Markdown generation, then later expand automatic analysis and richer workflows.
- Completed Stage 1: independent Project Chronicle module, Designer project selector, and explicit planning/development/bug writers.
- Completed Stage 2: added discussion, decision, and retrospective record writers with a scalable record type selector.
- Completed Stage 3: added Chronicle Guard mode to inject active Designer project context into agent prompts.
- Completed Stage 4: added a generated record browser and open action for Chronicle Markdown files.
- Completed Stage 5: added `chronicle.config.json` as a portable project record configuration file.
- Improved Chronicle Guard after user testing: default guard context, stricter project/record checks, question reasons, MVP-first guidance, candidate record output, and tests.
- Added Second Brain Trace Mode so users can verify whether active Brain notes were searched, referenced, and reflected in answers.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "g1nation",
"version": "2.34.0",
"version": "2.34.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "g1nation",
"version": "2.34.0",
"version": "2.34.1",
"license": "MIT",
"dependencies": {
"marked": "^18.0.2"
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "g1nation",
"displayName": "G1nation",
"description": "High-performance autonomous local AI coding agent for VS Code. Features vectorized inference, asynchronous task management, and 100% offline processing.",
"version": "2.34.0",
"version": "2.35.0",
"publisher": "connectailab",
"license": "MIT",
"icon": "assets/icon.png",
+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:
+199
View File
@@ -0,0 +1,199 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ProjectChronicleManager, ProjectProfile } from '../src/features/projectChronicle';
function makeProfile(root: string): ProjectProfile {
const now = '2026-05-02T00:00:00.000Z';
return {
projectId: 'test-project',
projectName: 'Test Project',
projectRoot: root,
recordRoot: path.join(root, 'records', 'Test Project'),
description: 'Test project records',
corePurpose: 'Verify chronicle records',
detailLevel: 'standard',
createdAt: now,
updatedAt: now
};
}
describe('ProjectChronicleManager', () => {
let tempRoot: string;
let manager: ProjectChronicleManager;
let profile: ProjectProfile;
beforeEach(() => {
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chronicle-'));
manager = new ProjectChronicleManager();
profile = makeProfile(tempRoot);
});
afterEach(() => {
fs.rmSync(tempRoot, { recursive: true, force: true });
});
it('creates the project record structure and seed files', () => {
manager.ensureProject(profile);
expect(fs.existsSync(path.join(profile.recordRoot, 'README.md'))).toBe(true);
expect(fs.existsSync(path.join(profile.recordRoot, 'chronicle.config.json'))).toBe(true);
expect(fs.existsSync(path.join(profile.recordRoot, 'project-profile.md'))).toBe(true);
expect(fs.existsSync(path.join(profile.recordRoot, 'timeline.md'))).toBe(true);
expect(fs.existsSync(path.join(profile.recordRoot, 'planning'))).toBe(true);
expect(fs.existsSync(path.join(profile.recordRoot, 'decisions'))).toBe(true);
expect(fs.existsSync(path.join(profile.recordRoot, 'development'))).toBe(true);
expect(fs.existsSync(path.join(profile.recordRoot, 'bugs'))).toBe(true);
const config = JSON.parse(fs.readFileSync(path.join(profile.recordRoot, 'chronicle.config.json'), 'utf8'));
expect(config.projectId).toBe(profile.projectId);
expect(config.recordRoot).toBe(profile.recordRoot);
});
it('writes planning records without overwriting existing files', () => {
const first = manager.writePlanning(profile, {
featureName: 'Record Guard',
purpose: 'Purpose',
background: 'Background',
userIntent: 'Intent',
scope: ['Scope'],
outOfScope: ['Out'],
developmentDirection: 'Direction',
dependencyStrategy: 'Dependency',
expectedValue: 'Value',
successCriteria: ['Success'],
developerInstruction: 'Instruction',
createdAt: '2026-05-02T00:00:00.000Z'
});
const second = manager.writePlanning(profile, {
featureName: 'Record Guard',
purpose: 'Purpose',
background: 'Background',
userIntent: 'Intent',
scope: ['Scope'],
outOfScope: ['Out'],
developmentDirection: 'Direction',
dependencyStrategy: 'Dependency',
expectedValue: 'Value',
successCriteria: ['Success'],
developerInstruction: 'Instruction',
createdAt: '2026-05-02T00:00:00.000Z'
});
expect(first.filePath).not.toBe(second.filePath);
expect(path.basename(second.filePath)).toBe('2026-05-02_record-guard-2.md');
});
it('increments ADR and bug numbers from existing files', () => {
manager.ensureProject(profile);
fs.writeFileSync(path.join(profile.recordRoot, 'decisions', 'ADR-0007-existing.md'), '# Existing\n');
fs.writeFileSync(path.join(profile.recordRoot, 'bugs', 'BUG-0003-existing.md'), '# Existing\n');
expect(manager.nextAdrNumber(profile)).toBe(8);
expect(manager.nextBugNumber(profile)).toBe(4);
});
it('writes discussion, decision, bug, development, and retrospective records', () => {
const discussion = manager.writeDiscussion(profile, {
title: 'Question Intent',
userRequest: 'Record why the AI asked a question.',
interpretedIntent: 'Preserve reasoning behind clarification.',
questions: [{
question: 'Which project should this be recorded under?',
reason: 'Records must not be mixed across projects.',
expectedInformation: 'The active project name and record path.',
impactOnDecision: 'Determines where generated Markdown is written.'
}],
discussions: ['The project context must be explicit before writing files.'],
decisions: [],
createdAt: '2026-05-02T00:00:00.000Z'
});
const decision = manager.writeDecision(profile, {
title: 'Use Markdown Records',
status: 'accepted',
context: 'Records should be portable.',
decision: 'Store records as Markdown.',
reason: 'Markdown works without external services.',
alternatives: ['External DB'],
consequences: ['Easy to inspect in the repository.'],
createdAt: '2026-05-02T00:00:00.000Z'
}, manager.nextAdrNumber(profile));
const development = manager.writeDevelopmentLog(profile, {
featureName: 'Chronicle Writer',
purpose: 'Verify write paths.',
implementationSummary: 'Wrote supported record types.',
architecture: 'Manager -> Writer -> Markdown.',
changedFiles: ['src/features/projectChronicle/index.ts'],
dependencyNotes: 'No external services.',
bugs: [],
lessons: ['Keep record types explicit.'],
createdAt: '2026-05-02T00:00:00.000Z'
});
const bug = manager.writeBug(profile, {
title: 'Missing Record Root',
symptom: 'Write cannot proceed.',
cause: 'Record root is empty.',
fix: 'Validate profile before write.',
prevention: 'Require selected project.',
createdAt: '2026-05-02T00:00:00.000Z'
}, manager.nextBugNumber(profile));
const retrospective = manager.writeRetrospective(profile, {
title: 'Chronicle Iteration',
summary: 'Record generation was expanded.',
wentWell: ['Independent module stayed small.'],
toImprove: ['Add richer UI later.'],
nextActions: ['Capture events automatically.'],
createdAt: '2026-05-02T00:00:00.000Z'
});
for (const result of [discussion, decision, development, bug, retrospective]) {
expect(fs.existsSync(result.filePath)).toBe(true);
}
expect(discussion.relativePath).toContain('discussions/');
expect(decision.relativePath).toContain('decisions/');
expect(development.relativePath).toContain('development/');
expect(bug.relativePath).toContain('bugs/');
expect(retrospective.relativePath).toContain('retrospectives/');
});
it('lists chronicle records across sections with relative paths', () => {
manager.writePlanning(profile, {
featureName: 'Record List',
purpose: 'Purpose',
background: 'Background',
userIntent: 'Intent',
scope: ['Scope'],
outOfScope: ['Out'],
developmentDirection: 'Direction',
dependencyStrategy: 'Dependency',
expectedValue: 'Value',
successCriteria: ['Success'],
developerInstruction: 'Instruction',
createdAt: '2026-05-02T00:00:00.000Z'
});
manager.writeBug(profile, {
title: 'List Bug',
symptom: 'Symptom',
cause: 'Cause',
fix: 'Fix',
prevention: 'Prevention',
createdAt: '2026-05-02T00:00:00.000Z'
}, 1);
const records = manager.listRecords(profile);
expect(records.length).toBe(2);
expect(records.map(record => record.relativePath)).toEqual(
expect.arrayContaining([
'planning/2026-05-02_record-list.md',
'bugs/BUG-0001-list-bug.md'
])
);
});
});
+37
View File
@@ -0,0 +1,37 @@
import { buildProjectChronicleGuardContext, ProjectProfile } from '../src/features/projectChronicle';
const profile: ProjectProfile = {
projectId: 'connectai',
projectName: 'ConnectAI',
projectRoot: '/workspace/ConnectAI',
recordRoot: '/workspace/ConnectAI/docs/records/ConnectAI',
description: 'Local AI assistant',
corePurpose: 'Keep project knowledge traceable.',
detailLevel: 'standard',
createdAt: '2026-05-02T00:00:00.000Z',
updatedAt: '2026-05-02T00:00:00.000Z'
};
describe('buildProjectChronicleGuardContext', () => {
it('requires project checks, question reasons, MVP-first scope, and candidate records', () => {
const context = buildProjectChronicleGuardContext(profile);
expect(context).toContain('Project selection status: selected');
expect(context).toContain('Project record target check');
expect(context).toContain('Record path check');
expect(context).toContain('Question reason');
expect(context).toContain('Recommend a low-dependency MVP first');
expect(context).toContain('Later expansion');
expect(context).toContain('Candidate records for this discussion');
expect(context).toContain('Do not mark a decision as accepted until the user confirms it');
expect(context).toContain('Markdown, JSON, local files');
});
it('handles missing project context explicitly', () => {
const context = buildProjectChronicleGuardContext(null);
expect(context).toContain('Project selection status: not selected');
expect(context).toContain('existing project, create a new project, or skip recording');
expect(context).toContain('must be described but not written');
});
});
+66
View File
@@ -0,0 +1,66 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
buildSecondBrainTrace,
renderSecondBrainTraceContext,
renderSecondBrainTraceMarkdown
} from '../src/features/secondBrainTrace';
describe('Second Brain Trace', () => {
let brainRoot: string;
beforeEach(() => {
brainRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'second-brain-trace-'));
fs.mkdirSync(path.join(brainRoot, 'records', 'ProjectChronicle', 'decisions'), { recursive: true });
fs.writeFileSync(
path.join(brainRoot, 'records', 'ProjectChronicle', 'decisions', 'ADR-0002-low-dependency-design.md'),
[
'# ADR-0002 Low Dependency Design',
'',
'Project Chronicle Guard should start with Markdown files and an independent module.',
'Vector DB and relational DB are later expansion options, not MVP dependencies.'
].join('\n'),
'utf8'
);
fs.writeFileSync(
path.join(brainRoot, 'general-note.md'),
'# General Note\n\nThis unrelated note talks about coffee and weather.',
'utf8'
);
});
afterEach(() => {
fs.rmSync(brainRoot, { recursive: true, force: true });
});
it('retrieves and marks relevant Second Brain notes for project-specific questions', () => {
const trace = buildSecondBrainTrace('Project Chronicle Guard MVP에서 Vector DB는 어떻게 다뤄야 해?', brainRoot);
expect(trace.shouldUseSecondBrain).toBe(true);
expect(trace.secondBrainUsed).toBe(true);
expect(trace.retrievedDocuments[0].path).toContain('ADR-0002-low-dependency-design.md');
expect(trace.retrievedDocuments[0].usedInAnswer).toBe(true);
expect(trace.groundingScore).toBeGreaterThan(0);
});
it('renders user-facing markdown and debug JSON', () => {
const trace = buildSecondBrainTrace('Second Brain을 참고해서 low dependency 원칙 알려줘', brainRoot, { force: true });
const markdown = renderSecondBrainTraceMarkdown(trace, true);
const context = renderSecondBrainTraceContext(trace);
expect(markdown).toContain('## 2nd Brain 사용 여부');
expect(markdown).toContain('## 참고한 2nd Brain 문서');
expect(markdown).toContain('## Second Brain Debug JSON');
expect(context).toContain('[SECOND BRAIN TRACE]');
expect(context).toContain('Retrieval query:');
});
it('explains when Second Brain is not needed', () => {
const trace = buildSecondBrainTrace('오늘 날짜가 뭐야?', brainRoot);
expect(trace.shouldUseSecondBrain).toBe(false);
expect(trace.secondBrainUsed).toBe(false);
expect(renderSecondBrainTraceMarkdown(trace)).toContain('사용하지 않음');
});
});