From 8bb8c065d77918a4039cbae642817ee7a2a1e1af Mon Sep 17 00:00:00 2001 From: g1nation Date: Sat, 2 May 2026 13:18:07 +0900 Subject: [PATCH] Bump version to 2.35.0: Knowledge Resilience & Standardization Milestone. --- PATCHNOTES.md | 18 + docs/records/ConnectAI/README.md | 18 + ...01-project-chronicle-independent-module.md | 27 + ...oject-chronicle-guard_feedback-response.md | 39 + ...-chronicle-guard_stage-1_implementation.md | 48 ++ ...-chronicle-guard_stage-2_implementation.md | 47 ++ ...nicle-guard_stage-3-to-5_implementation.md | 42 + ..._second-brain-trace-mode_implementation.md | 35 + .../2026-05-02_project-chronicle-guard.md | 27 + .../2026-05-02_project-chronicle-guard.md | 58 ++ .../2026-05-02_second-brain-trace-mode.md | 36 + docs/records/ConnectAI/project-profile.md | 30 + docs/records/ConnectAI/timeline.md | 13 + package-lock.json | 4 +- package.json | 2 +- src/agent.ts | 47 +- src/features/projectChronicle/guardPrompt.ts | 50 ++ src/features/projectChronicle/index.ts | 189 +++++ .../projectChronicle/markdownFileWriter.ts | 41 + src/features/projectChronicle/templates.ts | 258 ++++++ src/features/projectChronicle/types.ts | 104 +++ src/features/secondBrainTrace.ts | 264 ++++++ src/sidebarProvider.ts | 765 +++++++++++++++++- src/utils.ts | 2 + tests/projectChronicle.test.ts | 199 +++++ tests/projectChronicleGuardPrompt.test.ts | 37 + tests/secondBrainTrace.test.ts | 66 ++ 27 files changed, 2452 insertions(+), 14 deletions(-) create mode 100644 docs/records/ConnectAI/README.md create mode 100644 docs/records/ConnectAI/decisions/ADR-0001-project-chronicle-independent-module.md create mode 100644 docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_feedback-response.md create mode 100644 docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-1_implementation.md create mode 100644 docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-2_implementation.md create mode 100644 docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-3-to-5_implementation.md create mode 100644 docs/records/ConnectAI/development/2026-05-02_second-brain-trace-mode_implementation.md create mode 100644 docs/records/ConnectAI/discussions/2026-05-02_project-chronicle-guard.md create mode 100644 docs/records/ConnectAI/planning/2026-05-02_project-chronicle-guard.md create mode 100644 docs/records/ConnectAI/planning/2026-05-02_second-brain-trace-mode.md create mode 100644 docs/records/ConnectAI/project-profile.md create mode 100644 docs/records/ConnectAI/timeline.md create mode 100644 src/features/projectChronicle/guardPrompt.ts create mode 100644 src/features/projectChronicle/index.ts create mode 100644 src/features/projectChronicle/markdownFileWriter.ts create mode 100644 src/features/projectChronicle/templates.ts create mode 100644 src/features/projectChronicle/types.ts create mode 100644 src/features/secondBrainTrace.ts create mode 100644 tests/projectChronicle.test.ts create mode 100644 tests/projectChronicleGuardPrompt.test.ts create mode 100644 tests/secondBrainTrace.test.ts diff --git a/PATCHNOTES.md b/PATCHNOTES.md index a0cc107..e3b50ac 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -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 diff --git a/docs/records/ConnectAI/README.md b/docs/records/ConnectAI/README.md new file mode 100644 index 0000000..38d3d95 --- /dev/null +++ b/docs/records/ConnectAI/README.md @@ -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 diff --git a/docs/records/ConnectAI/decisions/ADR-0001-project-chronicle-independent-module.md b/docs/records/ConnectAI/decisions/ADR-0001-project-chronicle-independent-module.md new file mode 100644 index 0000000..98290fc --- /dev/null +++ b/docs/records/ConnectAI/decisions/ADR-0001-project-chronicle-independent-module.md @@ -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. diff --git a/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_feedback-response.md b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_feedback-response.md new file mode 100644 index 0000000..298e6b4 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_feedback-response.md @@ -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. diff --git a/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-1_implementation.md b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-1_implementation.md new file mode 100644 index 0000000..1120ba9 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-1_implementation.md @@ -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. diff --git a/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-2_implementation.md b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-2_implementation.md new file mode 100644 index 0000000..6512450 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-2_implementation.md @@ -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. diff --git a/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-3-to-5_implementation.md b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-3-to-5_implementation.md new file mode 100644 index 0000000..1173b5f --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-02_project-chronicle-guard_stage-3-to-5_implementation.md @@ -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 diff --git a/docs/records/ConnectAI/development/2026-05-02_second-brain-trace-mode_implementation.md b/docs/records/ConnectAI/development/2026-05-02_second-brain-trace-mode_implementation.md new file mode 100644 index 0000000..6462e14 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-02_second-brain-trace-mode_implementation.md @@ -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 diff --git a/docs/records/ConnectAI/discussions/2026-05-02_project-chronicle-guard.md b/docs/records/ConnectAI/discussions/2026-05-02_project-chronicle-guard.md new file mode 100644 index 0000000..cc98f11 --- /dev/null +++ b/docs/records/ConnectAI/discussions/2026-05-02_project-chronicle-guard.md @@ -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. diff --git a/docs/records/ConnectAI/planning/2026-05-02_project-chronicle-guard.md b/docs/records/ConnectAI/planning/2026-05-02_project-chronicle-guard.md new file mode 100644 index 0000000..9c4673d --- /dev/null +++ b/docs/records/ConnectAI/planning/2026-05-02_project-chronicle-guard.md @@ -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. diff --git a/docs/records/ConnectAI/planning/2026-05-02_second-brain-trace-mode.md b/docs/records/ConnectAI/planning/2026-05-02_second-brain-trace-mode.md new file mode 100644 index 0000000..73c7f4f --- /dev/null +++ b/docs/records/ConnectAI/planning/2026-05-02_second-brain-trace-mode.md @@ -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. diff --git a/docs/records/ConnectAI/project-profile.md b/docs/records/ConnectAI/project-profile.md new file mode 100644 index 0000000..8c1ac8c --- /dev/null +++ b/docs/records/ConnectAI/project-profile.md @@ -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 diff --git a/docs/records/ConnectAI/timeline.md b/docs/records/ConnectAI/timeline.md new file mode 100644 index 0000000..05158ee --- /dev/null +++ b/docs/records/ConnectAI/timeline.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 6098f49..abbcb50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 3a29233..e4f8577 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/agent.ts b/src/agent.ts index 59b4574..d1cc43d 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -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; diff --git a/src/features/projectChronicle/guardPrompt.ts b/src/features/projectChronicle/guardPrompt.ts new file mode 100644 index 0000000..c1d21e4 --- /dev/null +++ b/src/features/projectChronicle/guardPrompt.ts @@ -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'); +} diff --git a/src/features/projectChronicle/index.ts b/src/features/projectChronicle/index.ts new file mode 100644 index 0000000..32a335d --- /dev/null +++ b/src/features/projectChronicle/index.ts @@ -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); + } +} diff --git a/src/features/projectChronicle/markdownFileWriter.ts b/src/features/projectChronicle/markdownFileWriter.ts new file mode 100644 index 0000000..4762140 --- /dev/null +++ b/src/features/projectChronicle/markdownFileWriter.ts @@ -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'; + } +} diff --git a/src/features/projectChronicle/templates.ts b/src/features/projectChronicle/templates.ts new file mode 100644 index 0000000..018377a --- /dev/null +++ b/src/features/projectChronicle/templates.ts @@ -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'); +} diff --git a/src/features/projectChronicle/types.ts b/src/features/projectChronicle/types.ts new file mode 100644 index 0000000..1a370ac --- /dev/null +++ b/src/features/projectChronicle/types.ts @@ -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; +} diff --git a/src/features/secondBrainTrace.ts b/src/features/secondBrainTrace.ts new file mode 100644 index 0000000..fceef70 --- /dev/null +++ b/src/features/secondBrainTrace.ts @@ -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(); + 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 'ํ”„๋กœ์ ํŠธ ๊ณ ์œ  ๋งฅ๋ฝ ํ™•์ธ'; +} diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 446a1ae..170b87a 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -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(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(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
+ + + @@ -1786,6 +2404,35 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
+
+
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+
+ + +
+
@@ -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 --- diff --git a/src/utils.ts b/src/utils.ts index 4926e77..20130d3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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: diff --git a/tests/projectChronicle.test.ts b/tests/projectChronicle.test.ts new file mode 100644 index 0000000..8e30e7b --- /dev/null +++ b/tests/projectChronicle.test.ts @@ -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' + ]) + ); + }); +}); diff --git a/tests/projectChronicleGuardPrompt.test.ts b/tests/projectChronicleGuardPrompt.test.ts new file mode 100644 index 0000000..8e9ad15 --- /dev/null +++ b/tests/projectChronicleGuardPrompt.test.ts @@ -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'); + }); +}); diff --git a/tests/secondBrainTrace.test.ts b/tests/secondBrainTrace.test.ts new file mode 100644 index 0000000..5faf473 --- /dev/null +++ b/tests/secondBrainTrace.test.ts @@ -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('์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ'); + }); +});