Bump version to 2.35.0: Knowledge Resilience & Standardization Milestone.
This commit is contained in:
@@ -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)
|
# Patch Notes - v2.33.9 (2026-05-01)
|
||||||
|
|
||||||
## 🚀 Core Engine Upgrade
|
## 🚀 Core Engine Upgrade
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ConnectAI Project Chronicle Records
|
||||||
|
|
||||||
|
This folder stores planning, decision, development, bug, and retrospective records for ConnectAI.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
- Name: ConnectAI
|
||||||
|
- Root: `/Volumes/Data/project/Antigravity/ConnectAI`
|
||||||
|
- Record root: `docs/records/ConnectAI`
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
- `project-profile.md`: project identity and guardrails
|
||||||
|
- `timeline.md`: high-level chronological history
|
||||||
|
- `planning/`: feature planning records
|
||||||
|
- `discussions/`: AI-human discussion summaries
|
||||||
|
- `decisions/`: decision records
|
||||||
|
- `development/`: implementation logs
|
||||||
|
- `bugs/`: bug and fix records
|
||||||
|
- `retrospectives/`: post-work reviews
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# ADR-0001: Implement Project Chronicle Guard As An Independent Module
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The requested feature records project planning, questions, decisions, development logs, bugs, and retrospectives. Existing chat and agent systems already manage model interaction and agent skills.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Implement Project Chronicle Guard as a separate module under `src/features/projectChronicle`.
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
- It reduces the chance of regressions in chat and agent execution.
|
||||||
|
- It keeps the MVP focused on local Markdown generation.
|
||||||
|
- It can later receive events from chat or agents without owning those flows.
|
||||||
|
- It makes project-specific record storage easier to test and evolve.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
- Integrate into the existing Second Brain flow.
|
||||||
|
- Extend Agent Skill files to double as project records.
|
||||||
|
- Add a standalone Project Chronicle module.
|
||||||
|
|
||||||
|
## Selected Alternative
|
||||||
|
Add a standalone Project Chronicle module.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
The first stage needs explicit sidebar actions to create and write records. Automatic extraction can be layered on later.
|
||||||
+39
@@ -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.
|
||||||
+48
@@ -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.
|
||||||
+47
@@ -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.
|
||||||
+42
@@ -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
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
# Development Log: Second Brain Trace Mode
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Make Second Brain usage visible and testable in AI answers.
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
- Added `src/features/secondBrainTrace.ts`.
|
||||||
|
- Added trace scoring over active Second Brain Markdown files.
|
||||||
|
- Added user-facing trace Markdown with usage status, referenced documents, unused documents, and grounding metrics.
|
||||||
|
- Added debug JSON output when debug mode is enabled.
|
||||||
|
- Added Trace and Dbg sidebar buttons.
|
||||||
|
- Connected trace context into `AgentExecutor`.
|
||||||
|
- Added tests for retrieval, rendering, debug JSON, and no-use cases.
|
||||||
|
|
||||||
|
## Changed Files
|
||||||
|
- `src/features/secondBrainTrace.ts`
|
||||||
|
- `src/agent.ts`
|
||||||
|
- `src/sidebarProvider.ts`
|
||||||
|
- `tests/secondBrainTrace.test.ts`
|
||||||
|
- `docs/records/ConnectAI/planning/2026-05-02_second-brain-trace-mode.md`
|
||||||
|
- `docs/records/ConnectAI/development/2026-05-02_second-brain-trace-mode_implementation.md`
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- `./node_modules/.bin/tsc --noEmit`
|
||||||
|
- `npm run compile`
|
||||||
|
- `./node_modules/.bin/jest --runInBand`
|
||||||
|
|
||||||
|
## Result
|
||||||
|
When Trace mode is on, answers can now include:
|
||||||
|
- 2nd Brain usage status
|
||||||
|
- Reason for use or non-use
|
||||||
|
- Referenced note paths and excerpts
|
||||||
|
- Searched but unused notes
|
||||||
|
- Retrieval and grounding metrics
|
||||||
|
- Optional debug JSON
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# AI-Human Discussion Log
|
||||||
|
|
||||||
|
## Topic
|
||||||
|
Project Chronicle Guard sidebar Designer menu and Markdown record system.
|
||||||
|
|
||||||
|
## User Request Summary
|
||||||
|
The user wants a Designer menu in the sidebar and a staged implementation of a project planning, decision, and development record system.
|
||||||
|
|
||||||
|
## AI Questions
|
||||||
|
|
||||||
|
### Question
|
||||||
|
No blocking question was asked.
|
||||||
|
|
||||||
|
### Reason
|
||||||
|
The current workspace, project root, and a reasonable record location were available from local context.
|
||||||
|
|
||||||
|
### Impact On Decision
|
||||||
|
The first implementation can proceed with `ConnectAI` as the active project and `docs/records/ConnectAI` as the planning record location.
|
||||||
|
|
||||||
|
## Main Discussion
|
||||||
|
The feature should not replace code generation or existing agent execution. It should sit beside those features as a recording and decision support layer.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Use an independent `src/features/projectChronicle` module.
|
||||||
|
- Add a sidebar Designer control similar to existing Brain and Agent controls.
|
||||||
|
- Keep the MVP file-based and Markdown-only.
|
||||||
|
- Do not implement full automatic transcript capture in the first stage.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Feature Plan: Project Chronicle Guard
|
||||||
|
|
||||||
|
## 1. Feature Name
|
||||||
|
Project Chronicle Guard
|
||||||
|
|
||||||
|
## 2. Reason
|
||||||
|
The user wants AI-human planning, questions, decisions, implementation details, bugs, and retrospectives to remain as project-specific Markdown knowledge instead of disappearing into chat history.
|
||||||
|
|
||||||
|
## 3. Original User Request
|
||||||
|
Add a Designer menu to the sidebar, similar in spirit to Team, and implement a staged Project Chronicle Guard system that records planning, decisions, development logs, bug fixes, and retrospectives.
|
||||||
|
|
||||||
|
## 4. Interpreted Intent
|
||||||
|
The user does not only want another chat prompt preset. They want a durable project knowledge layer that captures why decisions were made and keeps records separated by project.
|
||||||
|
|
||||||
|
## 5. Problems To Solve
|
||||||
|
- Later readers forget why a direction was chosen.
|
||||||
|
- AI questions lose their intent and decision impact.
|
||||||
|
- Planning and implementation history are mixed or absent.
|
||||||
|
- Multiple projects can contaminate each other's records.
|
||||||
|
|
||||||
|
## 6. MVP Scope
|
||||||
|
- Add an independent `projectChronicle` module.
|
||||||
|
- Add project profile types and Markdown writer utilities.
|
||||||
|
- Add sidebar Designer controls for record project selection and basic project management.
|
||||||
|
- Generate the project folder structure and seed Markdown files.
|
||||||
|
- Save a planning document, discussion summary, decision record, development log, bug log, and timeline entry through explicit user actions.
|
||||||
|
|
||||||
|
## 7. Out Of Scope
|
||||||
|
- Full automatic transcript recording
|
||||||
|
- External database storage
|
||||||
|
- Git auto-commit
|
||||||
|
- Real-time file watching
|
||||||
|
- Search UI
|
||||||
|
- Tight integration with agent execution internals
|
||||||
|
|
||||||
|
## 8. Development Direction
|
||||||
|
Start with a low-risk independent module and connect the sidebar through message events. The chat agent can remain unaware of the record module in the first stage.
|
||||||
|
|
||||||
|
## 9. Existing Feature Relationship
|
||||||
|
The current sidebar already has Brain and Agent controls. The Designer menu should follow that UI pattern but manage project record profiles instead of agent skill files.
|
||||||
|
|
||||||
|
## 10. Dependency Strategy
|
||||||
|
Use TypeScript, Node `fs`, and Markdown files only. Store project profile metadata in VS Code global state and avoid modifying core chat execution.
|
||||||
|
|
||||||
|
## 11. Expected Value
|
||||||
|
- Project-specific planning and decision history becomes reusable.
|
||||||
|
- The user can intentionally create records before or after development work.
|
||||||
|
- Future automation can build on a stable local file structure.
|
||||||
|
|
||||||
|
## 12. Success Criteria
|
||||||
|
- A Designer selector appears in the sidebar.
|
||||||
|
- A new record project can be created.
|
||||||
|
- Record folders and seed files are generated safely.
|
||||||
|
- Markdown documents can be generated without needing external services.
|
||||||
|
- Existing chat, brain, and agent flows continue to compile.
|
||||||
|
|
||||||
|
## 13. Developer Instruction
|
||||||
|
Implement the first stage only: independent module, basic Designer menu, project creation/selection, folder seeding, and explicit sample record generation actions. Keep automatic conversation analysis for a later stage.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Feature Plan: Second Brain Trace Mode
|
||||||
|
|
||||||
|
## 1. Feature Name
|
||||||
|
Second Brain Trace Mode
|
||||||
|
|
||||||
|
## 2. Reason
|
||||||
|
The user wants to verify whether AI answers actually use Second Brain notes, which documents were searched, and how retrieved knowledge influenced the answer.
|
||||||
|
|
||||||
|
## 3. Original User Request
|
||||||
|
Add a way to see if the AI searched Second Brain, which notes it referenced, whether each note was used in the final answer, and why unused notes were excluded.
|
||||||
|
|
||||||
|
## 4. Interpreted User Intent
|
||||||
|
The user does not want to rely on vague claims that the AI used memory. They want visible grounding evidence and a developer debug trace.
|
||||||
|
|
||||||
|
## 5. Scope
|
||||||
|
- Add a trace mode toggle in the sidebar.
|
||||||
|
- Add a debug JSON toggle.
|
||||||
|
- Retrieve relevant Markdown notes from the active Second Brain.
|
||||||
|
- Inject selected notes into the answer context.
|
||||||
|
- Append a user-visible 2nd Brain usage section to answers.
|
||||||
|
- Show searched-but-unused notes and basic grounding metrics.
|
||||||
|
|
||||||
|
## 6. Out Of Scope
|
||||||
|
- Vector database integration
|
||||||
|
- Full semantic embedding search
|
||||||
|
- Perfect factual attribution at sentence level
|
||||||
|
- External observability tools
|
||||||
|
|
||||||
|
## 7. Development Direction
|
||||||
|
Use a low-dependency local Markdown search first. Keep vector search and richer grounding analysis as later expansion.
|
||||||
|
|
||||||
|
## 8. Success Criteria
|
||||||
|
- Users can see whether Second Brain was used.
|
||||||
|
- Users can see referenced Markdown paths.
|
||||||
|
- Debug mode exposes retrieval JSON.
|
||||||
|
- General questions can explicitly show that Second Brain was not used.
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "g1nation",
|
"name": "g1nation",
|
||||||
"version": "2.34.0",
|
"version": "2.34.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "g1nation",
|
"name": "g1nation",
|
||||||
"version": "2.34.0",
|
"version": "2.34.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked": "^18.0.2"
|
"marked": "^18.0.2"
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "g1nation",
|
"name": "g1nation",
|
||||||
"displayName": "G1nation",
|
"displayName": "G1nation",
|
||||||
"description": "High-performance autonomous local AI coding agent for VS Code. Features vectorized inference, asynchronous task management, and 100% offline processing.",
|
"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",
|
"publisher": "connectailab",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
+40
-7
@@ -32,6 +32,12 @@ import { StatusBarManager, AgentStatus } from './core/statusBar';
|
|||||||
import { lockManager } from './core/lock';
|
import { lockManager } from './core/lock';
|
||||||
import { actionQueue } from './core/queue';
|
import { actionQueue } from './core/queue';
|
||||||
import { ConflictResolver } from './core/conflict';
|
import { ConflictResolver } from './core/conflict';
|
||||||
|
import {
|
||||||
|
buildSecondBrainTrace,
|
||||||
|
renderSecondBrainTraceContext,
|
||||||
|
renderSecondBrainTraceMarkdown,
|
||||||
|
SecondBrainTrace
|
||||||
|
} from './features/secondBrainTrace';
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
role: 'user' | 'assistant' | 'system';
|
role: 'user' | 'assistant' | 'system';
|
||||||
@@ -182,7 +188,10 @@ export class AgentExecutor {
|
|||||||
systemPrompt?: string,
|
systemPrompt?: string,
|
||||||
runId?: number,
|
runId?: number,
|
||||||
agentSkillContext?: string,
|
agentSkillContext?: string,
|
||||||
negativePrompt?: string
|
negativePrompt?: string,
|
||||||
|
designerContext?: string,
|
||||||
|
secondBrainTraceEnabled?: boolean,
|
||||||
|
secondBrainTraceDebug?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
@@ -248,6 +257,13 @@ export class AgentExecutor {
|
|||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const activeBrain = getActiveBrainProfile();
|
const activeBrain = getActiveBrainProfile();
|
||||||
const brainFiles = findBrainFiles(activeBrain.localBrainPath);
|
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
|
const brainPreview = brainFiles
|
||||||
.slice(0, 30)
|
.slice(0, 30)
|
||||||
.map(file => path.relative(activeBrain.localBrainPath, file))
|
.map(file => path.relative(activeBrain.localBrainPath, file))
|
||||||
@@ -315,9 +331,15 @@ export class AgentExecutor {
|
|||||||
const negativeCtx = options.negativePrompt
|
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.]`
|
? `\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 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[] = [
|
const messagesForRequest: ChatMessage[] = [
|
||||||
{ role: 'system', content: fullSystemPrompt, internal: true },
|
{ role: 'system', content: fullSystemPrompt, internal: true },
|
||||||
...reqMessages
|
...reqMessages
|
||||||
@@ -403,7 +425,11 @@ export class AgentExecutor {
|
|||||||
// 5. Execute Actions
|
// 5. Execute Actions
|
||||||
const rationale = this.parseRationale(aiResponseText);
|
const rationale = this.parseRationale(aiResponseText);
|
||||||
const assistantContent = this.sanitizeAssistantContent(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.chatHistory.push(assistantMessage);
|
||||||
|
|
||||||
this.statusBarManager.updateStatus(AgentStatus.Executing);
|
this.statusBarManager.updateStatus(AgentStatus.Executing);
|
||||||
@@ -426,9 +452,9 @@ export class AgentExecutor {
|
|||||||
if (report.length === 0 && loopDepth === 0 && this.isUnproductiveWaitingReply(assistantContent)) {
|
if (report.length === 0 && loopDepth === 0 && this.isUnproductiveWaitingReply(assistantContent)) {
|
||||||
assistantMessage.internal = false;
|
assistantMessage.internal = false;
|
||||||
const correctedReply = await this.buildUnproductiveReplyCorrection(prompt || '');
|
const correctedReply = await this.buildUnproductiveReplyCorrection(prompt || '');
|
||||||
assistantMessage.content = correctedReply;
|
assistantMessage.content = traceMarkdown ? `${correctedReply}\n${traceMarkdown}` : correctedReply;
|
||||||
this.emitHistoryChanged();
|
this.emitHistoryChanged();
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: correctedReply });
|
this.webview.postMessage({ type: 'streamChunk', value: assistantMessage.content });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +488,7 @@ export class AgentExecutor {
|
|||||||
|
|
||||||
this.emitHistoryChanged();
|
this.emitHistoryChanged();
|
||||||
this.statusBarManager.updateStatus(AgentStatus.Success);
|
this.statusBarManager.updateStatus(AgentStatus.Success);
|
||||||
this.webview.postMessage({ type: 'streamChunk', value: assistantContent });
|
this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent });
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.statusBarManager.updateStatus(AgentStatus.Error, error.message);
|
this.statusBarManager.updateStatus(AgentStatus.Error, error.message);
|
||||||
@@ -517,12 +543,15 @@ export class AgentExecutor {
|
|||||||
const selectedAgentContext = options.agentSkillContext
|
const selectedAgentContext = options.agentSkillContext
|
||||||
? `\nSelected Agent Reference:\n${options.agentSkillContext}`
|
? `\nSelected Agent Reference:\n${options.agentSkillContext}`
|
||||||
: '';
|
: '';
|
||||||
|
const designerContext = options.designerContext
|
||||||
|
? `\nProject Chronicle Guard:\n${options.designerContext}`
|
||||||
|
: '';
|
||||||
|
|
||||||
// 워크플로우 매니저에게 설정 기반 실행 위임
|
// 워크플로우 매니저에게 설정 기반 실행 위임
|
||||||
const finalReport = await AgentWorkflowManager.runStrictWorkflow(
|
const finalReport = await AgentWorkflowManager.runStrictWorkflow(
|
||||||
prompt,
|
prompt,
|
||||||
modelName,
|
modelName,
|
||||||
`${brainContext}${selectedAgentContext}`,
|
`${brainContext}${selectedAgentContext}${designerContext}`,
|
||||||
signal,
|
signal,
|
||||||
(step, msg) => {
|
(step, msg) => {
|
||||||
this.webview?.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
|
this.webview?.postMessage({ type: 'autoContinue', value: `${step}: ${msg}` });
|
||||||
@@ -614,6 +643,10 @@ export class AgentExecutor {
|
|||||||
return mentionsBrain && asksOverview;
|
return mentionsBrain && asksOverview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isExplicitSecondBrainRequest(prompt: string): boolean {
|
||||||
|
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|노트|문서|참고해서|사용해서|검색해서|근거|출처)/i.test(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
private buildBrainOverviewReply(): string {
|
private buildBrainOverviewReply(): string {
|
||||||
const activeBrain = getActiveBrainProfile();
|
const activeBrain = getActiveBrainProfile();
|
||||||
const brainDir = activeBrain.localBrainPath;
|
const brainDir = activeBrain.localBrainPath;
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { ProjectProfile } from './types';
|
||||||
|
|
||||||
|
export function buildProjectChronicleGuardContext(project: ProjectProfile | null): string {
|
||||||
|
const hasUsableProject = !!project?.recordRoot?.trim();
|
||||||
|
const projectLines = project ? [
|
||||||
|
`Project selection status: selected`,
|
||||||
|
`Active record project: ${project.projectName}`,
|
||||||
|
`Project root: ${project.projectRoot || 'Not set'}`,
|
||||||
|
`Record root: ${project.recordRoot}`,
|
||||||
|
`Core purpose: ${project.corePurpose || project.description || 'Not captured yet.'}`,
|
||||||
|
`Record detail level: ${project.detailLevel}`
|
||||||
|
] : [
|
||||||
|
'Project selection status: not selected',
|
||||||
|
'No active record project is selected. Before writing records, ask the user to select or create one.'
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...projectLines,
|
||||||
|
'',
|
||||||
|
'This guard is active for project ideas, feature requests, architecture proposals, implementation planning, and design decisions.',
|
||||||
|
'',
|
||||||
|
'Required response order for new ideas or feature requests:',
|
||||||
|
'1. Request summary.',
|
||||||
|
'2. Inferred user intent.',
|
||||||
|
'3. Project record target check. If no project is selected, ask whether to use an existing project, create a new project, or skip recording.',
|
||||||
|
'4. Record path check. If no record root is available, say a Markdown record path is required before writing files.',
|
||||||
|
'5. Ask only 1 to 3 blocking questions.',
|
||||||
|
'6. For every question, include "Question reason" explaining why it changes storage, scope, dependencies, or implementation direction.',
|
||||||
|
'7. Direction review focused on project fit and dependency risk.',
|
||||||
|
'8. Recommend a low-dependency MVP first.',
|
||||||
|
'9. Put Vector DB, relational DB, knowledge graph, semantic search, and complex automation only under "Later expansion" unless the user explicitly asks for them now.',
|
||||||
|
'10. End with "Candidate records for this discussion" and list planning, discussions, decisions, development, bugs, or retrospectives paths as candidates.',
|
||||||
|
'',
|
||||||
|
'Decision policy:',
|
||||||
|
'- Do not mark a decision as accepted until the user confirms it.',
|
||||||
|
'- Before confirmation, call decisions "candidates" or "pending".',
|
||||||
|
'- Prefer "reduced adoption" when the idea is useful but too large for the MVP.',
|
||||||
|
'',
|
||||||
|
'Tone and scope:',
|
||||||
|
'- Be practical and plain-spoken.',
|
||||||
|
'- Avoid grand phrases like advanced cognitive architecture, compounding knowledge, perfect graph, or ultimate knowledge distiller.',
|
||||||
|
'- When the user wants low dependency, keep the first proposal to Markdown, JSON, local files, and explicit user save actions.',
|
||||||
|
'- Do not jump directly to large architectures. Narrow direction before expanding.',
|
||||||
|
'',
|
||||||
|
hasUsableProject
|
||||||
|
? 'The current project and record root are available, so candidate record paths may use this project.'
|
||||||
|
: 'The project or record root is missing, so candidate records must be described but not written.',
|
||||||
|
'Do not claim a Markdown file was written unless a tool or explicit sidebar action actually wrote it.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { MarkdownFileWriter } from './markdownFileWriter';
|
||||||
|
import {
|
||||||
|
BugRecord,
|
||||||
|
ChronicleRecordEntry,
|
||||||
|
ChronicleWriteResult,
|
||||||
|
DecisionRecord,
|
||||||
|
DevelopmentLog,
|
||||||
|
DiscussionRecord,
|
||||||
|
PlanningDocument,
|
||||||
|
ProjectProfile,
|
||||||
|
RetrospectiveRecord
|
||||||
|
} from './types';
|
||||||
|
import {
|
||||||
|
renderBugRecord,
|
||||||
|
renderDecisionRecord,
|
||||||
|
renderDevelopmentLog,
|
||||||
|
renderDiscussionRecord,
|
||||||
|
renderPlanningDocument,
|
||||||
|
renderProjectProfile,
|
||||||
|
renderProjectReadme,
|
||||||
|
renderRetrospective,
|
||||||
|
renderTimelineSeed
|
||||||
|
} from './templates';
|
||||||
|
|
||||||
|
export * from './types';
|
||||||
|
export * from './guardPrompt';
|
||||||
|
|
||||||
|
const sectionDirs = ['planning', 'discussions', 'decisions', 'development', 'bugs', 'retrospectives'];
|
||||||
|
|
||||||
|
export class ProjectChronicleManager {
|
||||||
|
private readonly writer = new MarkdownFileWriter();
|
||||||
|
|
||||||
|
public ensureProject(profile: ProjectProfile): void {
|
||||||
|
if (!profile.recordRoot || !profile.recordRoot.trim()) {
|
||||||
|
throw new Error('Record root is required before writing chronicle documents.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writer.ensureDir(profile.recordRoot);
|
||||||
|
for (const dir of sectionDirs) {
|
||||||
|
this.writer.ensureDir(path.join(profile.recordRoot, dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writeSeedFile(path.join(profile.recordRoot, 'README.md'), renderProjectReadme(profile));
|
||||||
|
this.writeSeedFile(path.join(profile.recordRoot, 'project-profile.md'), renderProjectProfile(profile));
|
||||||
|
this.writeSeedFile(path.join(profile.recordRoot, 'timeline.md'), renderTimelineSeed(profile));
|
||||||
|
this.writeProjectConfig(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public writePlanning(profile: ProjectProfile, doc: PlanningDocument): ChronicleWriteResult {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const date = this.datePart(doc.createdAt);
|
||||||
|
const fileName = `${date}_${this.slug(doc.featureName)}.md`;
|
||||||
|
return this.write(profile, 'planning', fileName, renderPlanningDocument(doc));
|
||||||
|
}
|
||||||
|
|
||||||
|
public writeDiscussion(profile: ProjectProfile, record: DiscussionRecord): ChronicleWriteResult {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const date = this.datePart(record.createdAt);
|
||||||
|
const fileName = `${date}_${this.slug(record.title)}.md`;
|
||||||
|
return this.write(profile, 'discussions', fileName, renderDiscussionRecord(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
public writeDecision(profile: ProjectProfile, record: DecisionRecord, adrNumber: number): ChronicleWriteResult {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const padded = String(adrNumber).padStart(4, '0');
|
||||||
|
const fileName = `ADR-${padded}-${this.slug(record.title)}.md`;
|
||||||
|
return this.write(profile, 'decisions', fileName, renderDecisionRecord(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
public writeDevelopmentLog(profile: ProjectProfile, log: DevelopmentLog): ChronicleWriteResult {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const date = this.datePart(log.createdAt);
|
||||||
|
const fileName = `${date}_${this.slug(log.featureName)}_implementation.md`;
|
||||||
|
return this.write(profile, 'development', fileName, renderDevelopmentLog(log));
|
||||||
|
}
|
||||||
|
|
||||||
|
public writeBug(profile: ProjectProfile, record: BugRecord, bugNumber: number): ChronicleWriteResult {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const padded = String(bugNumber).padStart(4, '0');
|
||||||
|
const fileName = `BUG-${padded}-${this.slug(record.title)}.md`;
|
||||||
|
return this.write(profile, 'bugs', fileName, renderBugRecord(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
public writeRetrospective(profile: ProjectProfile, record: RetrospectiveRecord): ChronicleWriteResult {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const date = this.datePart(record.createdAt);
|
||||||
|
const fileName = `${date}_${this.slug(record.title)}.md`;
|
||||||
|
return this.write(profile, 'retrospectives', fileName, renderRetrospective(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendTimeline(profile: ProjectProfile, lines: string[], createdAt: string = new Date().toISOString()): void {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const timelinePath = path.join(profile.recordRoot, 'timeline.md');
|
||||||
|
const markdown = [
|
||||||
|
'',
|
||||||
|
`## ${this.datePart(createdAt)}`,
|
||||||
|
...lines.map(line => `- ${line}`)
|
||||||
|
].join('\n');
|
||||||
|
this.writer.appendMarkdown(timelinePath, markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextAdrNumber(profile: ProjectProfile): number {
|
||||||
|
return this.nextNumber(path.join(profile.recordRoot, 'decisions'), /^ADR-(\d+)/);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBugNumber(profile: ProjectProfile): number {
|
||||||
|
return this.nextNumber(path.join(profile.recordRoot, 'bugs'), /^BUG-(\d+)/);
|
||||||
|
}
|
||||||
|
|
||||||
|
public listRecords(profile: ProjectProfile): ChronicleRecordEntry[] {
|
||||||
|
this.ensureProject(profile);
|
||||||
|
const records: ChronicleRecordEntry[] = [];
|
||||||
|
|
||||||
|
for (const section of sectionDirs) {
|
||||||
|
const sectionPath = path.join(profile.recordRoot, section);
|
||||||
|
if (!fs.existsSync(sectionPath)) continue;
|
||||||
|
|
||||||
|
for (const fileName of fs.readdirSync(sectionPath)) {
|
||||||
|
if (!fileName.endsWith('.md')) continue;
|
||||||
|
const filePath = path.join(sectionPath, fileName);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
records.push({
|
||||||
|
section,
|
||||||
|
fileName,
|
||||||
|
filePath,
|
||||||
|
relativePath: path.relative(profile.recordRoot, filePath),
|
||||||
|
updatedAt: stat.mtimeMs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private write(profile: ProjectProfile, section: string, fileName: string, markdown: string): ChronicleWriteResult {
|
||||||
|
const filePath = this.writer.writeMarkdown(path.join(profile.recordRoot, section, fileName), markdown);
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
relativePath: path.relative(profile.recordRoot, filePath)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeSeedFile(filePath: string, content: string): void {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
this.writer.writeMarkdown(filePath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeProjectConfig(profile: ProjectProfile): void {
|
||||||
|
const configPath = path.join(profile.recordRoot, 'chronicle.config.json');
|
||||||
|
const config = {
|
||||||
|
projectId: profile.projectId,
|
||||||
|
projectName: profile.projectName,
|
||||||
|
projectRoot: profile.projectRoot || '',
|
||||||
|
recordRoot: profile.recordRoot,
|
||||||
|
description: profile.description || '',
|
||||||
|
corePurpose: profile.corePurpose || '',
|
||||||
|
detailLevel: profile.detailLevel,
|
||||||
|
createdAt: profile.createdAt,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextNumber(dir: string, pattern: RegExp): number {
|
||||||
|
if (!fs.existsSync(dir)) return 1;
|
||||||
|
const max = fs.readdirSync(dir).reduce((current, fileName) => {
|
||||||
|
const match = fileName.match(pattern);
|
||||||
|
if (!match) return current;
|
||||||
|
return Math.max(current, Number(match[1]) || 0);
|
||||||
|
}, 0);
|
||||||
|
return max + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private slug(value: string): string {
|
||||||
|
const slug = value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9가-힣]+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '')
|
||||||
|
.slice(0, 60);
|
||||||
|
return slug || 'record';
|
||||||
|
}
|
||||||
|
|
||||||
|
private datePart(iso?: string): string {
|
||||||
|
return (iso || new Date().toISOString()).slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export class MarkdownFileWriter {
|
||||||
|
public ensureDir(dirPath: string): void {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public writeMarkdown(filePath: string, content: string): string {
|
||||||
|
this.ensureDir(path.dirname(filePath));
|
||||||
|
const finalPath = this.getAvailablePath(filePath);
|
||||||
|
fs.writeFileSync(finalPath, this.normalize(content), 'utf8');
|
||||||
|
return finalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendMarkdown(filePath: string, content: string): void {
|
||||||
|
this.ensureDir(path.dirname(filePath));
|
||||||
|
fs.appendFileSync(filePath, this.normalize(content), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAvailablePath(filePath: string): string {
|
||||||
|
if (!fs.existsSync(filePath)) return filePath;
|
||||||
|
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const base = path.basename(filePath, ext);
|
||||||
|
let index = 2;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const candidate = path.join(dir, `${base}-${index}${ext}`);
|
||||||
|
if (!fs.existsSync(candidate)) return candidate;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalize(content: string): string {
|
||||||
|
return content.replace(/\r\n/g, '\n').trimEnd() + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { findBrainFiles, summarizeText } from '../utils';
|
||||||
|
|
||||||
|
export interface SecondBrainTraceDocument {
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
absolutePath: string;
|
||||||
|
score: number;
|
||||||
|
excerpt: string;
|
||||||
|
usedInAnswer: boolean;
|
||||||
|
usedFor?: string;
|
||||||
|
excludedReason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecondBrainTrace {
|
||||||
|
userQuery: string;
|
||||||
|
shouldUseSecondBrain: boolean;
|
||||||
|
secondBrainUsed: boolean;
|
||||||
|
reason: string;
|
||||||
|
retrievalQuery: string;
|
||||||
|
searchedCollections: string[];
|
||||||
|
retrievedDocuments: SecondBrainTraceDocument[];
|
||||||
|
groundingScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSecondBrainTrace(userQuery: string, brainRoot: string, options: {
|
||||||
|
force?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
} = {}): SecondBrainTrace {
|
||||||
|
const query = userQuery.trim();
|
||||||
|
const shouldUseSecondBrain = !!options.force || shouldUseBrain(query);
|
||||||
|
const retrievalQuery = buildRetrievalQuery(query);
|
||||||
|
const baseTrace: SecondBrainTrace = {
|
||||||
|
userQuery: query,
|
||||||
|
shouldUseSecondBrain,
|
||||||
|
secondBrainUsed: false,
|
||||||
|
reason: shouldUseSecondBrain
|
||||||
|
? 'Project-specific or memory-sensitive information may be needed.'
|
||||||
|
: 'This looks answerable without project-specific Second Brain context.',
|
||||||
|
retrievalQuery,
|
||||||
|
searchedCollections: [],
|
||||||
|
retrievedDocuments: [],
|
||||||
|
groundingScore: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!shouldUseSecondBrain) return baseTrace;
|
||||||
|
if (!brainRoot || !fs.existsSync(brainRoot)) {
|
||||||
|
return {
|
||||||
|
...baseTrace,
|
||||||
|
reason: 'Second Brain was requested, but the active brain folder does not exist.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = findBrainFiles(brainRoot);
|
||||||
|
const terms = tokenize(retrievalQuery);
|
||||||
|
const scored = files.map((file) => scoreFile(file, brainRoot, terms))
|
||||||
|
.filter((doc) => doc.score > 0)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, options.limit || 5);
|
||||||
|
|
||||||
|
const usedDocs = scored.slice(0, Math.min(3, scored.length)).map((doc) => ({
|
||||||
|
...doc,
|
||||||
|
usedInAnswer: true,
|
||||||
|
usedFor: inferUsedFor(doc.excerpt)
|
||||||
|
}));
|
||||||
|
const unusedDocs = scored.slice(usedDocs.length).map((doc) => ({
|
||||||
|
...doc,
|
||||||
|
usedInAnswer: false,
|
||||||
|
excludedReason: 'Lower relevance than the documents selected as answer context.'
|
||||||
|
}));
|
||||||
|
const retrievedDocuments = [...usedDocs, ...unusedDocs];
|
||||||
|
const usedCount = retrievedDocuments.filter((doc) => doc.usedInAnswer).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseTrace,
|
||||||
|
secondBrainUsed: retrievedDocuments.length > 0,
|
||||||
|
reason: retrievedDocuments.length > 0
|
||||||
|
? 'Relevant Markdown notes were found and selected as answer context.'
|
||||||
|
: 'Second Brain search ran, but no sufficiently relevant Markdown notes were found.',
|
||||||
|
searchedCollections: inferCollections(retrievedDocuments),
|
||||||
|
retrievedDocuments,
|
||||||
|
groundingScore: retrievedDocuments.length === 0
|
||||||
|
? 0
|
||||||
|
: Number((usedCount / retrievedDocuments.length).toFixed(2))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSecondBrainTraceContext(trace: SecondBrainTrace): string {
|
||||||
|
if (!trace.shouldUseSecondBrain) {
|
||||||
|
return [
|
||||||
|
'[SECOND BRAIN TRACE]',
|
||||||
|
'Second Brain was not used for this request.',
|
||||||
|
`Reason: ${trace.reason}`,
|
||||||
|
'If the user explicitly asks to use Second Brain or asks project-specific memory questions, use it.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = trace.retrievedDocuments
|
||||||
|
.filter((doc) => doc.usedInAnswer)
|
||||||
|
.map((doc) => [
|
||||||
|
`- ${doc.path}`,
|
||||||
|
` Score: ${doc.score}`,
|
||||||
|
` Relevant content: ${doc.excerpt}`
|
||||||
|
].join('\n'))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'[SECOND BRAIN TRACE]',
|
||||||
|
`Second Brain used: ${trace.secondBrainUsed ? 'yes' : 'no'}`,
|
||||||
|
`Retrieval query: ${trace.retrievalQuery}`,
|
||||||
|
`Reason: ${trace.reason}`,
|
||||||
|
docs ? `Selected notes:\n${docs}` : 'Selected notes: none',
|
||||||
|
'',
|
||||||
|
'When answering, use only selected notes that are relevant. If these notes influence the answer, mention them in the final reference section.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSecondBrainTraceMarkdown(trace: SecondBrainTrace, debug: boolean = false): string {
|
||||||
|
const usedDocs = trace.retrievedDocuments.filter((doc) => doc.usedInAnswer);
|
||||||
|
const unusedDocs = trace.retrievedDocuments.filter((doc) => !doc.usedInAnswer);
|
||||||
|
const usedText = usedDocs.length
|
||||||
|
? usedDocs.map((doc) => [
|
||||||
|
`- \`${doc.path}\``,
|
||||||
|
` - Score: ${doc.score}`,
|
||||||
|
` - 참고 내용: ${doc.excerpt}`
|
||||||
|
].join('\n')).join('\n')
|
||||||
|
: '- 없음';
|
||||||
|
const unusedText = unusedDocs.length
|
||||||
|
? unusedDocs.map((doc) => [
|
||||||
|
`- \`${doc.path}\``,
|
||||||
|
` - 제외 이유: ${doc.excludedReason || '이번 답변의 핵심 근거로 선택되지 않았습니다.'}`
|
||||||
|
].join('\n')).join('\n')
|
||||||
|
: '- 없음';
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
'',
|
||||||
|
'## 2nd Brain 사용 여부',
|
||||||
|
trace.secondBrainUsed ? '사용함' : '사용하지 않음',
|
||||||
|
'',
|
||||||
|
'## 이유',
|
||||||
|
trace.reason,
|
||||||
|
'',
|
||||||
|
'## 참고한 2nd Brain 문서',
|
||||||
|
usedText,
|
||||||
|
'',
|
||||||
|
'## 검색했지만 사용하지 않은 문서',
|
||||||
|
unusedText,
|
||||||
|
'',
|
||||||
|
'## 참고 품질',
|
||||||
|
`- 검색된 노트: ${trace.retrievedDocuments.length}개`,
|
||||||
|
`- 실제 사용된 노트: ${usedDocs.length}개`,
|
||||||
|
`- 답변 근거도: ${trace.groundingScore}`
|
||||||
|
];
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
sections.push(
|
||||||
|
'',
|
||||||
|
'## Second Brain Debug JSON',
|
||||||
|
'```json',
|
||||||
|
JSON.stringify({
|
||||||
|
secondBrainUsed: trace.secondBrainUsed,
|
||||||
|
shouldUseSecondBrain: trace.shouldUseSecondBrain,
|
||||||
|
retrievalQuery: trace.retrievalQuery,
|
||||||
|
searchedCollections: trace.searchedCollections,
|
||||||
|
retrievedDocuments: trace.retrievedDocuments.map((doc) => ({
|
||||||
|
path: doc.path,
|
||||||
|
score: doc.score,
|
||||||
|
usedInAnswer: doc.usedInAnswer,
|
||||||
|
usedFor: doc.usedFor,
|
||||||
|
excludedReason: doc.excludedReason
|
||||||
|
})),
|
||||||
|
groundingScore: trace.groundingScore
|
||||||
|
}, null, 2),
|
||||||
|
'```'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseBrain(query: string): boolean {
|
||||||
|
const normalized = query.toLowerCase();
|
||||||
|
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|문서|노트|내가|우리|프로젝트|결정|adr|chronicle|가드|설계 원칙|mvp|제외|왜)/i.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRetrievalQuery(query: string): string {
|
||||||
|
return tokenize(query).slice(0, 16).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize(value: string): string[] {
|
||||||
|
const stopWords = new Set(['그리고', '그런데', '해서', '하는', '있어', 'what', 'how', 'the', 'and', 'for', 'with']);
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/[^a-z0-9가-힣_]+/g)
|
||||||
|
.map((term) => term.trim())
|
||||||
|
.filter((term) => term.length >= 2 && !stopWords.has(term));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreFile(file: string, brainRoot: string, terms: string[]): SecondBrainTraceDocument {
|
||||||
|
const relative = path.relative(brainRoot, file);
|
||||||
|
const title = path.basename(file, path.extname(file));
|
||||||
|
const basename = relative.toLowerCase();
|
||||||
|
let content = '';
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(file, 'utf8');
|
||||||
|
} catch {
|
||||||
|
content = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = content.toLowerCase();
|
||||||
|
let score = 0;
|
||||||
|
for (const term of terms) {
|
||||||
|
if (basename.includes(term)) score += 4;
|
||||||
|
const matches = lower.split(term).length - 1;
|
||||||
|
if (matches > 0) score += Math.min(matches, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
path: relative,
|
||||||
|
absolutePath: file,
|
||||||
|
score: Number((score / Math.max(terms.length, 1)).toFixed(2)),
|
||||||
|
excerpt: summarizeText(bestExcerpt(content, terms), 420),
|
||||||
|
usedInAnswer: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestExcerpt(content: string, terms: string[]): string {
|
||||||
|
const paragraphs = content
|
||||||
|
.split(/\n\s*\n/g)
|
||||||
|
.map((part) => part.replace(/\s+/g, ' ').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (paragraphs.length === 0) return '';
|
||||||
|
|
||||||
|
let best = paragraphs[0];
|
||||||
|
let bestScore = -1;
|
||||||
|
for (const paragraph of paragraphs) {
|
||||||
|
const lower = paragraph.toLowerCase();
|
||||||
|
const score = terms.reduce((sum, term) => sum + (lower.includes(term) ? 1 : 0), 0);
|
||||||
|
if (score > bestScore) {
|
||||||
|
best = paragraph;
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferCollections(docs: SecondBrainTraceDocument[]): string[] {
|
||||||
|
const collections = new Set<string>();
|
||||||
|
for (const doc of docs) {
|
||||||
|
const first = doc.path.split(/[\\/]/)[0];
|
||||||
|
if (first) collections.add(first);
|
||||||
|
}
|
||||||
|
return Array.from(collections);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferUsedFor(excerpt: string): string {
|
||||||
|
if (/의존|coupl|독립|분리/i.test(excerpt)) return '의존도와 독립 모듈 판단';
|
||||||
|
if (/markdown|마크다운/i.test(excerpt)) return 'Markdown 기반 저장 방향';
|
||||||
|
if (/질문|의도|reason/i.test(excerpt)) return '질문 의도와 기록 방식';
|
||||||
|
if (/mvp|제외|scope/i.test(excerpt)) return 'MVP 범위 판단';
|
||||||
|
return '프로젝트 고유 맥락 확인';
|
||||||
|
}
|
||||||
+761
-4
@@ -15,6 +15,7 @@ import {
|
|||||||
import { getConfig } from './config';
|
import { getConfig } from './config';
|
||||||
import { AgentExecutor, ChatMessage } from './agent';
|
import { AgentExecutor, ChatMessage } from './agent';
|
||||||
import { BridgeInterface } from './bridge';
|
import { BridgeInterface } from './bridge';
|
||||||
|
import { buildProjectChronicleGuardContext, ProjectChronicleManager, ProjectProfile } from './features/projectChronicle';
|
||||||
|
|
||||||
interface LastVisibleChatSnapshot {
|
interface LastVisibleChatSnapshot {
|
||||||
history: ChatMessage[];
|
history: ChatMessage[];
|
||||||
@@ -42,10 +43,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
private static readonly lastVisibleChatStateKey = 'g1nation.lastVisibleChat';
|
private static readonly lastVisibleChatStateKey = 'g1nation.lastVisibleChat';
|
||||||
private static readonly blankChatStateKey = 'g1nation.blankChatActive';
|
private static readonly blankChatStateKey = 'g1nation.blankChatActive';
|
||||||
private static readonly lastAgentStateKey = 'g1nation.lastAgentPath';
|
private static readonly lastAgentStateKey = 'g1nation.lastAgentPath';
|
||||||
|
private static readonly chronicleProjectsStateKey = 'g1nation.chronicleProjects';
|
||||||
|
private static readonly activeChronicleProjectStateKey = 'g1nation.activeChronicleProjectId';
|
||||||
private _view?: vscode.WebviewView;
|
private _view?: vscode.WebviewView;
|
||||||
public brainEnabled = true;
|
public brainEnabled = true;
|
||||||
private _currentSessionBrainId: string | null = null;
|
private _currentSessionBrainId: string | null = null;
|
||||||
private _currentNegativePrompt: string = '';
|
private _currentNegativePrompt: string = '';
|
||||||
|
private readonly _chronicle = new ProjectChronicleManager();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _extensionUri: vscode.Uri,
|
private readonly _extensionUri: vscode.Uri,
|
||||||
@@ -89,6 +93,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
await this._sendSessionList();
|
await this._sendSessionList();
|
||||||
await this._sendModels();
|
await this._sendModels();
|
||||||
await this._sendConfig();
|
await this._sendConfig();
|
||||||
|
await this._sendChronicleProjects();
|
||||||
await this._restoreActiveSessionIntoView();
|
await this._restoreActiveSessionIntoView();
|
||||||
break;
|
break;
|
||||||
case 'toggleMultiAgent':
|
case 'toggleMultiAgent':
|
||||||
@@ -103,6 +108,45 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
case 'getAgents':
|
case 'getAgents':
|
||||||
await this._sendAgentsList();
|
await this._sendAgentsList();
|
||||||
break;
|
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':
|
case 'createAgent':
|
||||||
await this._createAgent();
|
await this._createAgent();
|
||||||
break;
|
break;
|
||||||
@@ -899,6 +943,568 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getChronicleProjects(): ProjectProfile[] {
|
||||||
|
const raw = this._context.globalState.get<ProjectProfile[]>(SidebarChatProvider.chronicleProjectsStateKey, []) || [];
|
||||||
|
const valid = raw.filter((profile: ProjectProfile) =>
|
||||||
|
profile
|
||||||
|
&& typeof profile.projectId === 'string'
|
||||||
|
&& typeof profile.projectName === 'string'
|
||||||
|
&& typeof profile.recordRoot === 'string'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (valid.length > 0) return valid;
|
||||||
|
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||||
|
if (!workspaceRoot) return [];
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const projectName = path.basename(workspaceRoot) || 'Current Project';
|
||||||
|
return [{
|
||||||
|
projectId: this._slugify(projectName),
|
||||||
|
projectName,
|
||||||
|
projectRoot: workspaceRoot,
|
||||||
|
recordRoot: path.join(workspaceRoot, 'docs', 'records', projectName),
|
||||||
|
description: 'Auto-detected current workspace project.',
|
||||||
|
corePurpose: 'Capture project planning, decisions, development notes, bugs, and retrospectives as Markdown.',
|
||||||
|
targetUsers: ['Project developer'],
|
||||||
|
avoidDirections: ['Do not tightly couple records to chat execution internals.'],
|
||||||
|
detailLevel: 'standard',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _putChronicleProjects(projects: ProjectProfile[]) {
|
||||||
|
await this._context.globalState.update(SidebarChatProvider.chronicleProjectsStateKey, projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getActiveChronicleProject(): ProjectProfile | null {
|
||||||
|
const projects = this._getChronicleProjects();
|
||||||
|
if (projects.length === 0) return null;
|
||||||
|
const activeId = this._context.globalState.get<string>(SidebarChatProvider.activeChronicleProjectStateKey, '');
|
||||||
|
return projects.find(project => project.projectId === activeId) || projects[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _sendChronicleProjects() {
|
||||||
|
if (!this._view) return;
|
||||||
|
const projects = this._getChronicleProjects();
|
||||||
|
const active = this._getActiveChronicleProject();
|
||||||
|
this._view.webview.postMessage({
|
||||||
|
type: 'chronicleProjects',
|
||||||
|
value: {
|
||||||
|
activeProjectId: active?.projectId || '',
|
||||||
|
projects: projects.map(project => ({
|
||||||
|
id: project.projectId,
|
||||||
|
name: project.projectName,
|
||||||
|
root: project.projectRoot || '',
|
||||||
|
recordRoot: project.recordRoot,
|
||||||
|
description: project.description || ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createChronicleProject() {
|
||||||
|
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
|
||||||
|
const defaultName = workspaceRoot ? path.basename(workspaceRoot) : 'New Project';
|
||||||
|
|
||||||
|
const projectName = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Project name for Chronicle records',
|
||||||
|
value: defaultName,
|
||||||
|
validateInput: (value) => value.trim() ? null : 'Project name is required.'
|
||||||
|
});
|
||||||
|
if (!projectName) return;
|
||||||
|
|
||||||
|
const description = await vscode.window.showInputBox({
|
||||||
|
prompt: 'One-line project description',
|
||||||
|
value: 'Project planning, decisions, development logs, and bug records.'
|
||||||
|
});
|
||||||
|
if (description === undefined) return;
|
||||||
|
|
||||||
|
const projectRoot = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Project root path',
|
||||||
|
value: workspaceRoot,
|
||||||
|
validateInput: (value) => value.trim() ? null : 'Project root is required.'
|
||||||
|
});
|
||||||
|
if (!projectRoot) return;
|
||||||
|
|
||||||
|
const defaultRecordRoot = path.join(projectRoot.trim(), 'docs', 'records', projectName.trim());
|
||||||
|
const recordRoot = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Markdown record folder path',
|
||||||
|
value: defaultRecordRoot,
|
||||||
|
validateInput: (value) => value.trim() ? null : 'Record folder path is required.'
|
||||||
|
});
|
||||||
|
if (!recordRoot) return;
|
||||||
|
|
||||||
|
const corePurpose = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Core project purpose or guardrail',
|
||||||
|
value: 'Keep project knowledge traceable through Markdown records.'
|
||||||
|
});
|
||||||
|
if (corePurpose === undefined) return;
|
||||||
|
|
||||||
|
const detailChoice = await vscode.window.showQuickPick([
|
||||||
|
{ label: 'standard', description: 'Request, question intent, decisions, planning, development, and bugs' },
|
||||||
|
{ label: 'simple', description: 'Request summary, decisions, and implementation result' },
|
||||||
|
{ label: 'detailed', description: 'Standard records plus alternatives, lessons, and retrospectives' }
|
||||||
|
], {
|
||||||
|
placeHolder: 'Chronicle record detail level'
|
||||||
|
});
|
||||||
|
if (!detailChoice) return;
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const projects = this._getChronicleProjects();
|
||||||
|
const idBase = this._slugify(projectName.trim());
|
||||||
|
let projectId = idBase;
|
||||||
|
let suffix = 2;
|
||||||
|
while (projects.some(project => project.projectId === projectId)) {
|
||||||
|
projectId = `${idBase}-${suffix++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile: ProjectProfile = {
|
||||||
|
projectId,
|
||||||
|
projectName: projectName.trim(),
|
||||||
|
projectRoot: projectRoot.trim(),
|
||||||
|
recordRoot: recordRoot.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
corePurpose: corePurpose.trim(),
|
||||||
|
targetUsers: ['Project developer'],
|
||||||
|
avoidDirections: ['Do not mix records across projects.', 'Do not tightly couple records to core agent execution.'],
|
||||||
|
detailLevel: detailChoice.label as ProjectProfile['detailLevel'],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
};
|
||||||
|
|
||||||
|
this._chronicle.ensureProject(profile);
|
||||||
|
const nextProjects = [...projects.filter(project => project.projectId !== profile.projectId), profile];
|
||||||
|
await this._putChronicleProjects(nextProjects);
|
||||||
|
await this._context.globalState.update(SidebarChatProvider.activeChronicleProjectStateKey, profile.projectId);
|
||||||
|
await this._sendChronicleProjects();
|
||||||
|
this.injectSystemMessage(`**[Designer Project Created]** ${profile.projectName}\n\`${profile.recordRoot}\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _setActiveChronicleProject(projectId: string) {
|
||||||
|
if (!projectId || projectId === 'new') {
|
||||||
|
await this._createChronicleProject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = this._getChronicleProjects().find(project => project.projectId === projectId);
|
||||||
|
if (!target) return;
|
||||||
|
await this._context.globalState.update(SidebarChatProvider.activeChronicleProjectStateKey, target.projectId);
|
||||||
|
await this._sendChronicleProjects();
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
this.injectSystemMessage(`**[Designer Project Selected]** ${target.projectName}\n\`${target.recordRoot}\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _openChronicleFolder() {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
vscode.window.showWarningMessage('No Chronicle project is selected.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._chronicle.ensureProject(profile);
|
||||||
|
await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(profile.recordRoot));
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to open Chronicle folder: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _sendChronicleRecords() {
|
||||||
|
if (!this._view) return;
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
this._view.webview.postMessage({ type: 'chronicleRecords', value: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const records = this._chronicle.listRecords(profile).map(record => ({
|
||||||
|
section: record.section,
|
||||||
|
fileName: record.fileName,
|
||||||
|
path: record.filePath,
|
||||||
|
relativePath: record.relativePath,
|
||||||
|
updatedAt: record.updatedAt
|
||||||
|
}));
|
||||||
|
this._view.webview.postMessage({ type: 'chronicleRecords', value: records });
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to list Chronicle records: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _openChronicleRecord(recordPath: string) {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile || !recordPath) {
|
||||||
|
vscode.window.showWarningMessage('Select a Chronicle record first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = path.resolve(profile.recordRoot);
|
||||||
|
const target = path.resolve(recordPath);
|
||||||
|
if (!target.startsWith(`${root}${path.sep}`) || path.extname(target) !== '.md') {
|
||||||
|
vscode.window.showErrorMessage('Selected Chronicle record path is not valid.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(target)) {
|
||||||
|
vscode.window.showErrorMessage('Selected Chronicle record no longer exists.');
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await vscode.workspace.openTextDocument(target);
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _writeChroniclePlanningFromCurrentChat() {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = this._agent.getHistory();
|
||||||
|
const latestUser = [...history].reverse().find(message => message.role === 'user')?.content || '';
|
||||||
|
const latestAssistant = [...history].reverse().find(message => message.role === 'assistant')?.content || '';
|
||||||
|
const featureName = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Feature name for the planning document',
|
||||||
|
value: this._summarizeForTitle(latestUser || 'Project Chronicle Feature')
|
||||||
|
});
|
||||||
|
if (!featureName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAt = new Date().toISOString();
|
||||||
|
const result = this._chronicle.writePlanning(profile, {
|
||||||
|
featureName: featureName.trim(),
|
||||||
|
purpose: 'Record the reason, scope, direction, and success criteria before implementation.',
|
||||||
|
background: this._summarizeTextForWiki(latestAssistant || latestUser),
|
||||||
|
userIntent: this._summarizeTextForWiki(latestUser),
|
||||||
|
sourceRequest: latestUser || 'No user request captured in the current chat.',
|
||||||
|
scope: [
|
||||||
|
'Create a project-specific planning record.',
|
||||||
|
'Capture user intent and implementation direction.',
|
||||||
|
'Keep the record independent from chat execution internals.'
|
||||||
|
],
|
||||||
|
outOfScope: [
|
||||||
|
'Full automatic transcript capture.',
|
||||||
|
'External database integration.',
|
||||||
|
'Git automation.'
|
||||||
|
],
|
||||||
|
developmentDirection: 'Use Project Chronicle as a low-dependency Markdown record layer.',
|
||||||
|
dependencyStrategy: 'Use local filesystem writes through the independent projectChronicle module.',
|
||||||
|
expectedValue: 'Future work can understand why this feature exists and what decisions shaped it.',
|
||||||
|
successCriteria: [
|
||||||
|
'The planning document is created under the selected project record folder.',
|
||||||
|
'The document includes user intent, scope, out-of-scope items, and success criteria.'
|
||||||
|
],
|
||||||
|
developerInstruction: 'Use this document as the implementation guardrail for the next development step.',
|
||||||
|
createdAt
|
||||||
|
});
|
||||||
|
this._chronicle.appendTimeline(profile, [`Planning record created: ${result.relativePath}`], createdAt);
|
||||||
|
vscode.window.showInformationMessage(`Chronicle planning saved: ${result.relativePath}`);
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
this.injectSystemMessage(`**[Chronicle Planning Saved]** \`${result.filePath}\``);
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to write planning record: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _writeChronicleDiscussionFromCurrentChat() {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = this._agent.getHistory();
|
||||||
|
const latestUser = [...history].reverse().find(message => message.role === 'user')?.content || '';
|
||||||
|
const latestAssistant = [...history].reverse().find(message => message.role === 'assistant')?.content || '';
|
||||||
|
const title = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Discussion title',
|
||||||
|
value: this._summarizeForTitle(latestUser || 'Project Discussion')
|
||||||
|
});
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
const question = await vscode.window.showInputBox({
|
||||||
|
prompt: 'AI question to record (optional)',
|
||||||
|
value: ''
|
||||||
|
});
|
||||||
|
if (question === undefined) return;
|
||||||
|
|
||||||
|
let questions: any[] = [];
|
||||||
|
if (question.trim()) {
|
||||||
|
const reason = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Why was this question asked?',
|
||||||
|
value: 'To avoid writing records to the wrong project or making an unclear design decision.'
|
||||||
|
});
|
||||||
|
if (reason === undefined) return;
|
||||||
|
|
||||||
|
const impact = await vscode.window.showInputBox({
|
||||||
|
prompt: 'How does this question affect the decision?',
|
||||||
|
value: 'It determines the correct project context, scope, or implementation path.'
|
||||||
|
});
|
||||||
|
if (impact === undefined) return;
|
||||||
|
|
||||||
|
questions = [{
|
||||||
|
question: question.trim(),
|
||||||
|
reason: reason.trim(),
|
||||||
|
expectedInformation: 'Information needed to clarify project context, scope, or decision direction.',
|
||||||
|
impactOnDecision: impact.trim()
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAt = new Date().toISOString();
|
||||||
|
const result = this._chronicle.writeDiscussion(profile, {
|
||||||
|
title: title.trim(),
|
||||||
|
userRequest: this._summarizeTextForWiki(latestUser || 'No user request captured in the current chat.'),
|
||||||
|
interpretedIntent: 'Capture the discussion as a reusable project knowledge record instead of leaving it only in chat history.',
|
||||||
|
questions,
|
||||||
|
discussions: [
|
||||||
|
this._summarizeTextForWiki(latestAssistant || latestUser || 'No discussion detail captured yet.')
|
||||||
|
],
|
||||||
|
decisions: [],
|
||||||
|
createdAt
|
||||||
|
});
|
||||||
|
this._chronicle.appendTimeline(profile, [`Discussion record created: ${result.relativePath}`], createdAt);
|
||||||
|
vscode.window.showInformationMessage(`Chronicle discussion saved: ${result.relativePath}`);
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
this.injectSystemMessage(`**[Chronicle Discussion Saved]** \`${result.filePath}\``);
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to write discussion record: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _writeChronicleDecisionFromInput() {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Decision title',
|
||||||
|
value: 'Use independent Markdown record module'
|
||||||
|
});
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
const decision = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Decision',
|
||||||
|
value: 'Implement this behavior as an independent Project Chronicle module.'
|
||||||
|
});
|
||||||
|
if (decision === undefined) return;
|
||||||
|
|
||||||
|
const reason = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Decision reason',
|
||||||
|
value: 'To reduce coupling and keep project records portable.'
|
||||||
|
});
|
||||||
|
if (reason === undefined) return;
|
||||||
|
|
||||||
|
const alternatives = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Rejected alternatives (comma-separated)',
|
||||||
|
value: 'Integrate with Second Brain, integrate directly into Agent execution'
|
||||||
|
});
|
||||||
|
if (alternatives === undefined) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAt = new Date().toISOString();
|
||||||
|
const adrNumber = this._chronicle.nextAdrNumber(profile);
|
||||||
|
const result = this._chronicle.writeDecision(profile, {
|
||||||
|
title: title.trim(),
|
||||||
|
status: 'accepted',
|
||||||
|
context: 'A project record needs to capture not only what changed, but why the direction was chosen.',
|
||||||
|
decision: decision.trim(),
|
||||||
|
reason: reason.trim(),
|
||||||
|
alternatives: alternatives.split(',').map(item => item.trim()).filter(Boolean),
|
||||||
|
consequences: [
|
||||||
|
'Records can evolve independently from chat and agent internals.',
|
||||||
|
'Future automation can emit chronicle events without owning the core execution path.'
|
||||||
|
],
|
||||||
|
createdAt
|
||||||
|
}, adrNumber);
|
||||||
|
this._chronicle.appendTimeline(profile, [`Decision record created: ${result.relativePath}`], createdAt);
|
||||||
|
vscode.window.showInformationMessage(`Chronicle decision saved: ${result.relativePath}`);
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
this.injectSystemMessage(`**[Chronicle Decision Saved]** \`${result.filePath}\``);
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to write decision record: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _writeChronicleDevelopmentFromCurrentChat() {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = this._agent.getHistory();
|
||||||
|
const latestUser = [...history].reverse().find(message => message.role === 'user')?.content || '';
|
||||||
|
const latestAssistant = [...history].reverse().find(message => message.role === 'assistant')?.content || '';
|
||||||
|
const featureName = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Feature name for the development log',
|
||||||
|
value: this._summarizeForTitle(latestUser || 'Implementation Log')
|
||||||
|
});
|
||||||
|
if (!featureName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAt = new Date().toISOString();
|
||||||
|
const result = this._chronicle.writeDevelopmentLog(profile, {
|
||||||
|
featureName: featureName.trim(),
|
||||||
|
purpose: 'Record the actual implementation outcome for later maintenance.',
|
||||||
|
implementationSummary: this._summarizeTextForWiki(latestAssistant || 'Implementation summary was not captured from chat yet.'),
|
||||||
|
architecture: 'Project Chronicle records are written through an independent Markdown module.',
|
||||||
|
changedFiles: ['Capture exact changed files after verification.'],
|
||||||
|
dependencyNotes: 'Keep dependencies limited to TypeScript, Node fs/path, and VS Code extension APIs.',
|
||||||
|
bugs: [],
|
||||||
|
lessons: [
|
||||||
|
'Write implementation notes as soon as a stable development step finishes.',
|
||||||
|
'Keep generated records project-specific.'
|
||||||
|
],
|
||||||
|
createdAt
|
||||||
|
});
|
||||||
|
this._chronicle.appendTimeline(profile, [`Development log created: ${result.relativePath}`], createdAt);
|
||||||
|
vscode.window.showInformationMessage(`Chronicle development log saved: ${result.relativePath}`);
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
this.injectSystemMessage(`**[Chronicle Development Saved]** \`${result.filePath}\``);
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to write development record: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _writeChronicleBugFromInput() {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Bug title',
|
||||||
|
value: 'record-generation-issue'
|
||||||
|
});
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
const symptom = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Bug symptom',
|
||||||
|
value: 'Describe what failed or looked wrong.'
|
||||||
|
});
|
||||||
|
if (symptom === undefined) return;
|
||||||
|
|
||||||
|
const cause = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Bug cause',
|
||||||
|
value: 'Cause is not confirmed yet.'
|
||||||
|
});
|
||||||
|
if (cause === undefined) return;
|
||||||
|
|
||||||
|
const fix = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Fix',
|
||||||
|
value: 'Describe the fix or mitigation.'
|
||||||
|
});
|
||||||
|
if (fix === undefined) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAt = new Date().toISOString();
|
||||||
|
const bugNumber = this._chronicle.nextBugNumber(profile);
|
||||||
|
const result = this._chronicle.writeBug(profile, {
|
||||||
|
title: title.trim(),
|
||||||
|
symptom: symptom.trim(),
|
||||||
|
cause: cause.trim(),
|
||||||
|
fix: fix.trim(),
|
||||||
|
prevention: 'Validate project selection, record path, and write permissions before generating files.',
|
||||||
|
createdAt
|
||||||
|
}, bugNumber);
|
||||||
|
this._chronicle.appendTimeline(profile, [`Bug record created: ${result.relativePath}`], createdAt);
|
||||||
|
vscode.window.showInformationMessage(`Chronicle bug record saved: ${result.relativePath}`);
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
this.injectSystemMessage(`**[Chronicle Bug Saved]** \`${result.filePath}\``);
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to write bug record: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _writeChronicleRetrospectiveFromInput() {
|
||||||
|
const profile = this._getActiveChronicleProject();
|
||||||
|
if (!profile) {
|
||||||
|
vscode.window.showWarningMessage('Select or create a Designer project first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Retrospective title',
|
||||||
|
value: 'Project Chronicle Guard iteration'
|
||||||
|
});
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
const summary = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Work summary',
|
||||||
|
value: 'Completed an incremental development step and recorded the outcome.'
|
||||||
|
});
|
||||||
|
if (summary === undefined) return;
|
||||||
|
|
||||||
|
const wentWell = await vscode.window.showInputBox({
|
||||||
|
prompt: 'What went well? (comma-separated)',
|
||||||
|
value: 'Kept the feature independent, Generated Markdown records, Preserved project context'
|
||||||
|
});
|
||||||
|
if (wentWell === undefined) return;
|
||||||
|
|
||||||
|
const toImprove = await vscode.window.showInputBox({
|
||||||
|
prompt: 'What should improve? (comma-separated)',
|
||||||
|
value: 'More automatic question intent capture, Richer record editing UI'
|
||||||
|
});
|
||||||
|
if (toImprove === undefined) return;
|
||||||
|
|
||||||
|
const nextActions = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Next actions (comma-separated)',
|
||||||
|
value: 'Add tests, Improve Designer UI, Add event-based record capture'
|
||||||
|
});
|
||||||
|
if (nextActions === undefined) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAt = new Date().toISOString();
|
||||||
|
const result = this._chronicle.writeRetrospective(profile, {
|
||||||
|
title: title.trim(),
|
||||||
|
summary: summary.trim(),
|
||||||
|
wentWell: wentWell.split(',').map(item => item.trim()).filter(Boolean),
|
||||||
|
toImprove: toImprove.split(',').map(item => item.trim()).filter(Boolean),
|
||||||
|
nextActions: nextActions.split(',').map(item => item.trim()).filter(Boolean),
|
||||||
|
createdAt
|
||||||
|
});
|
||||||
|
this._chronicle.appendTimeline(profile, [`Retrospective created: ${result.relativePath}`], createdAt);
|
||||||
|
vscode.window.showInformationMessage(`Chronicle retrospective saved: ${result.relativePath}`);
|
||||||
|
await this._sendChronicleRecords();
|
||||||
|
this.injectSystemMessage(`**[Chronicle Retrospective Saved]** \`${result.filePath}\``);
|
||||||
|
} catch (err: any) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to write retrospective record: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _writeChronicleRecord(recordType: string) {
|
||||||
|
switch (recordType) {
|
||||||
|
case 'planning':
|
||||||
|
await this._writeChroniclePlanningFromCurrentChat();
|
||||||
|
break;
|
||||||
|
case 'discussion':
|
||||||
|
await this._writeChronicleDiscussionFromCurrentChat();
|
||||||
|
break;
|
||||||
|
case 'decision':
|
||||||
|
await this._writeChronicleDecisionFromInput();
|
||||||
|
break;
|
||||||
|
case 'development':
|
||||||
|
await this._writeChronicleDevelopmentFromCurrentChat();
|
||||||
|
break;
|
||||||
|
case 'bug':
|
||||||
|
await this._writeChronicleBugFromInput();
|
||||||
|
break;
|
||||||
|
case 'retrospective':
|
||||||
|
await this._writeChronicleRetrospectiveFromInput();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
vscode.window.showWarningMessage('Select a Chronicle record type first.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _getAgentsDir(): string {
|
private _getAgentsDir(): string {
|
||||||
const defaultPath = 'E:\\Wiki\\Agent\\.agent\\skills';
|
const defaultPath = 'E:\\Wiki\\Agent\\.agent\\skills';
|
||||||
if (fs.existsSync(defaultPath)) return defaultPath;
|
if (fs.existsSync(defaultPath)) return defaultPath;
|
||||||
@@ -1043,7 +1649,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
private async _handlePrompt(data: any) {
|
private async _handlePrompt(data: any) {
|
||||||
if (!this._view) return;
|
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._currentNegativePrompt = negativePrompt || '';
|
||||||
this._currentSessionBrainId = getActiveBrainProfile().id;
|
this._currentSessionBrainId = getActiveBrainProfile().id;
|
||||||
|
|
||||||
@@ -1052,12 +1658,17 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
agentSkillContext = fs.readFileSync(agentFile, 'utf8');
|
agentSkillContext = fs.readFileSync(agentFile, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const designerContext = designerGuard !== false ? this._buildDesignerGuardContext() : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._agent.handlePrompt(value, model, {
|
await this._agent.handlePrompt(value, model, {
|
||||||
internetEnabled: internet,
|
internetEnabled: internet,
|
||||||
visionContent: files,
|
visionContent: files,
|
||||||
agentSkillContext,
|
agentSkillContext,
|
||||||
negativePrompt
|
negativePrompt,
|
||||||
|
designerContext,
|
||||||
|
secondBrainTraceEnabled: secondBrainTrace !== false,
|
||||||
|
secondBrainTraceDebug: !!secondBrainTraceDebug
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logError('Prompt handling failed in sidebar provider.', { error: error?.message || String(error), promptPreview: summarizeText(value || '', 200) });
|
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() {
|
private async _sendModels() {
|
||||||
if (!this._view) return;
|
if (!this._view) return;
|
||||||
try {
|
try {
|
||||||
@@ -1755,6 +2370,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="icon-btn" id="newChatBtn" data-tooltip="New Chat">New</button>
|
<button class="icon-btn" id="newChatBtn" data-tooltip="New Chat">New</button>
|
||||||
<button class="icon-btn" id="saveWikiRawBtn" data-tooltip="Save Wiki Raw">Wiki</button>
|
<button class="icon-btn" id="saveWikiRawBtn" data-tooltip="Save Wiki Raw">Wiki</button>
|
||||||
|
<button class="icon-btn active" id="designerGuardBtn" data-tooltip="Chronicle Guard Mode: Auto">Guard</button>
|
||||||
|
<button class="icon-btn active" id="brainTraceBtn" data-tooltip="Second Brain Trace Mode">Trace</button>
|
||||||
|
<button class="icon-btn" id="brainTraceDebugBtn" data-tooltip="Second Brain Debug JSON">Dbg</button>
|
||||||
<button class="icon-btn" id="multiAgentBtn" data-tooltip="Multi-Agent Mode">MA</button>
|
<button class="icon-btn" id="multiAgentBtn" data-tooltip="Multi-Agent Mode">MA</button>
|
||||||
<button class="icon-btn" id="internetBtn" data-tooltip="Internet Access">Web</button>
|
<button class="icon-btn" id="internetBtn" data-tooltip="Internet Access">Web</button>
|
||||||
<button class="icon-btn" id="historyBtn" data-tooltip="View History">Log</button>
|
<button class="icon-btn" id="historyBtn" data-tooltip="View History">Log</button>
|
||||||
@@ -1786,6 +2404,35 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="select-wrap"><select id="designerSel" title="Select Designer Project"></select></div>
|
||||||
|
<div class="tool-group" aria-label="Designer actions">
|
||||||
|
<button class="icon-btn" id="addDesignerBtn" data-tooltip="Create Designer Project">Add</button>
|
||||||
|
<button class="icon-btn" id="openDesignerBtn" data-tooltip="Open Record Folder">Open</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="select-wrap">
|
||||||
|
<select id="chronicleRecordTypeSel" title="Select Chronicle Record Type">
|
||||||
|
<option value="planning">Planning</option>
|
||||||
|
<option value="discussion">Discussion</option>
|
||||||
|
<option value="decision">Decision</option>
|
||||||
|
<option value="development">Development</option>
|
||||||
|
<option value="bug">Bug</option>
|
||||||
|
<option value="retrospective">Retrospective</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="tool-group" aria-label="Chronicle write actions">
|
||||||
|
<button class="icon-btn" id="writeChronicleBtn" data-tooltip="Write Selected Record">Write</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-row">
|
||||||
|
<div class="select-wrap"><select id="chronicleRecordSel" title="Select Chronicle Record"></select></div>
|
||||||
|
<div class="tool-group" aria-label="Chronicle record actions">
|
||||||
|
<button class="icon-btn" id="refreshChronicleRecordsBtn" data-tooltip="Refresh Records">Ref</button>
|
||||||
|
<button class="icon-btn" id="openChronicleRecordBtn" data-tooltip="Open Selected Record">Open</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1864,7 +2511,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveWebviewState(history) {
|
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) {
|
function renderHistory(history) {
|
||||||
@@ -1989,6 +2642,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const attachPreview = document.getElementById('attachPreview');
|
const attachPreview = document.getElementById('attachPreview');
|
||||||
const agentSel = document.getElementById('agentSel');
|
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 editAgentBtn = document.getElementById('editAgentBtn');
|
||||||
const addAgentBtn = document.getElementById('addAgentBtn');
|
const addAgentBtn = document.getElementById('addAgentBtn');
|
||||||
const deleteAgentBtn = document.getElementById('deleteAgentBtn');
|
const deleteAgentBtn = document.getElementById('deleteAgentBtn');
|
||||||
@@ -2003,8 +2659,29 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
|
|
||||||
let streamBody = null;
|
let streamBody = null;
|
||||||
let internetEnabled = false;
|
let internetEnabled = false;
|
||||||
|
let designerGuardEnabled = true;
|
||||||
|
let secondBrainTraceEnabled = true;
|
||||||
|
let secondBrainTraceDebug = false;
|
||||||
let pendingFiles = [];
|
let pendingFiles = [];
|
||||||
let editMode = false;
|
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 || ''); }
|
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 });
|
vscode.postMessage({ type: 'getAgentContent', path: msg.selected });
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'agentContent':
|
||||||
agentPrompt.value = msg.value;
|
agentPrompt.value = msg.value;
|
||||||
negativePrompt.value = msg.negativePrompt || '';
|
negativePrompt.value = msg.negativePrompt || '';
|
||||||
@@ -2324,7 +3034,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
internet: internetEnabled,
|
internet: internetEnabled,
|
||||||
files: pendingFiles.length > 0 ? pendingFiles : undefined,
|
files: pendingFiles.length > 0 ? pendingFiles : undefined,
|
||||||
agentFile: agentSel.value === 'none' ? undefined : agentSel.value,
|
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();
|
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
|
||||||
// 전송 완료 후 Draft State 리셋 + Stop 버튼 표시
|
// 전송 완료 후 Draft State 리셋 + Stop 버튼 표시
|
||||||
@@ -2370,6 +3083,27 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
document.getElementById('internetBtn').onclick = () => {
|
document.getElementById('internetBtn').onclick = () => {
|
||||||
internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled);
|
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;
|
let multiAgentEnabled = false;
|
||||||
const setMultiAgentUi = (enabled) => {
|
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
|
// Handle initial state and state updates from extension
|
||||||
window.addEventListener('message', e => {
|
window.addEventListener('message', e => {
|
||||||
const msg = e.data;
|
const msg = e.data;
|
||||||
@@ -2473,8 +3216,22 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
vscode.postMessage({ type: 'deleteAgent', path: agentSel.value });
|
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: 'getModels' });
|
||||||
vscode.postMessage({ type: 'getAgents' });
|
vscode.postMessage({ type: 'getAgents' });
|
||||||
|
vscode.postMessage({ type: 'getChronicleProjects' });
|
||||||
|
vscode.postMessage({ type: 'getChronicleRecords' });
|
||||||
vscode.postMessage({ type: 'ready' });
|
vscode.postMessage({ type: 'ready' });
|
||||||
|
|
||||||
// --- Proactive Behavioral Tracking ---
|
// --- Proactive Behavioral Tracking ---
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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.
|
- 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:
|
Available action tags:
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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('사용하지 않음');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user