chati-dev 1.4.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/framework/agents/build/dev.md +343 -0
- package/framework/agents/clarity/architect.md +112 -0
- package/framework/agents/clarity/brief.md +182 -0
- package/framework/agents/clarity/brownfield-wu.md +181 -0
- package/framework/agents/clarity/detail.md +110 -0
- package/framework/agents/clarity/greenfield-wu.md +153 -0
- package/framework/agents/clarity/ux.md +112 -0
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +31 -1
- package/framework/context/governance.md +37 -0
- package/framework/context/protocols.md +34 -0
- package/framework/context/quality.md +27 -0
- package/framework/context/root.md +24 -0
- package/framework/domains/agents/architect.yaml +51 -0
- package/framework/domains/agents/brief.yaml +47 -0
- package/framework/domains/agents/brownfield-wu.yaml +49 -0
- package/framework/domains/agents/detail.yaml +47 -0
- package/framework/domains/agents/dev.yaml +49 -0
- package/framework/domains/agents/devops.yaml +43 -0
- package/framework/domains/agents/greenfield-wu.yaml +47 -0
- package/framework/domains/agents/orchestrator.yaml +49 -0
- package/framework/domains/agents/phases.yaml +47 -0
- package/framework/domains/agents/qa-implementation.yaml +43 -0
- package/framework/domains/agents/qa-planning.yaml +44 -0
- package/framework/domains/agents/tasks.yaml +48 -0
- package/framework/domains/agents/ux.yaml +50 -0
- package/framework/domains/constitution.yaml +77 -0
- package/framework/domains/global.yaml +64 -0
- package/framework/domains/workflows/brownfield-discovery.yaml +16 -0
- package/framework/domains/workflows/brownfield-fullstack.yaml +26 -0
- package/framework/domains/workflows/brownfield-service.yaml +22 -0
- package/framework/domains/workflows/brownfield-ui.yaml +22 -0
- package/framework/domains/workflows/greenfield-fullstack.yaml +26 -0
- package/framework/hooks/constitution-guard.js +101 -0
- package/framework/hooks/mode-governance.js +92 -0
- package/framework/hooks/model-governance.js +76 -0
- package/framework/hooks/prism-engine.js +89 -0
- package/framework/hooks/session-digest.js +60 -0
- package/framework/hooks/settings.json +44 -0
- package/framework/migrations/v1.4-to-v2.0.yaml +167 -0
- package/framework/migrations/v2.0-to-v2.0.1.yaml +132 -0
- package/framework/orchestrator/chati.md +284 -6
- package/framework/tasks/architect-api-design.md +63 -0
- package/framework/tasks/architect-consolidate.md +47 -0
- package/framework/tasks/architect-db-design.md +73 -0
- package/framework/tasks/architect-design.md +95 -0
- package/framework/tasks/architect-security-review.md +62 -0
- package/framework/tasks/architect-stack-selection.md +53 -0
- package/framework/tasks/brief-consolidate.md +249 -0
- package/framework/tasks/brief-constraint-identify.md +277 -0
- package/framework/tasks/brief-extract-requirements.md +339 -0
- package/framework/tasks/brief-stakeholder-map.md +176 -0
- package/framework/tasks/brief-validate-completeness.md +121 -0
- package/framework/tasks/brownfield-wu-architecture-map.md +394 -0
- package/framework/tasks/brownfield-wu-deep-discovery.md +312 -0
- package/framework/tasks/brownfield-wu-dependency-scan.md +359 -0
- package/framework/tasks/brownfield-wu-migration-plan.md +483 -0
- package/framework/tasks/brownfield-wu-report.md +325 -0
- package/framework/tasks/brownfield-wu-risk-assess.md +424 -0
- package/framework/tasks/detail-acceptance-criteria.md +372 -0
- package/framework/tasks/detail-consolidate.md +138 -0
- package/framework/tasks/detail-edge-case-analysis.md +300 -0
- package/framework/tasks/detail-expand-prd.md +389 -0
- package/framework/tasks/detail-nfr-extraction.md +223 -0
- package/framework/tasks/dev-code-review.md +404 -0
- package/framework/tasks/dev-consolidate.md +543 -0
- package/framework/tasks/dev-debug.md +322 -0
- package/framework/tasks/dev-implement.md +252 -0
- package/framework/tasks/dev-iterate.md +411 -0
- package/framework/tasks/dev-pr-prepare.md +497 -0
- package/framework/tasks/dev-refactor.md +342 -0
- package/framework/tasks/dev-test-write.md +306 -0
- package/framework/tasks/devops-ci-setup.md +412 -0
- package/framework/tasks/devops-consolidate.md +712 -0
- package/framework/tasks/devops-deploy-config.md +598 -0
- package/framework/tasks/devops-monitoring-setup.md +658 -0
- package/framework/tasks/devops-release-prepare.md +673 -0
- package/framework/tasks/greenfield-wu-analyze-empty.md +169 -0
- package/framework/tasks/greenfield-wu-report.md +266 -0
- package/framework/tasks/greenfield-wu-scaffold-detection.md +203 -0
- package/framework/tasks/greenfield-wu-tech-stack-assess.md +255 -0
- package/framework/tasks/orchestrator-deviation.md +260 -0
- package/framework/tasks/orchestrator-escalate.md +276 -0
- package/framework/tasks/orchestrator-handoff.md +243 -0
- package/framework/tasks/orchestrator-health.md +372 -0
- package/framework/tasks/orchestrator-mode-switch.md +262 -0
- package/framework/tasks/orchestrator-resume.md +189 -0
- package/framework/tasks/orchestrator-route.md +169 -0
- package/framework/tasks/orchestrator-spawn-terminal.md +358 -0
- package/framework/tasks/orchestrator-status.md +260 -0
- package/framework/tasks/orchestrator-suggest-mode.md +372 -0
- package/framework/tasks/phases-breakdown.md +91 -0
- package/framework/tasks/phases-dependency-mapping.md +67 -0
- package/framework/tasks/phases-mvp-scoping.md +94 -0
- package/framework/tasks/qa-impl-consolidate.md +522 -0
- package/framework/tasks/qa-impl-performance-test.md +487 -0
- package/framework/tasks/qa-impl-regression-check.md +413 -0
- package/framework/tasks/qa-impl-sast-scan.md +402 -0
- package/framework/tasks/qa-impl-test-execute.md +344 -0
- package/framework/tasks/qa-impl-verdict.md +339 -0
- package/framework/tasks/qa-planning-consolidate.md +309 -0
- package/framework/tasks/qa-planning-coverage-plan.md +338 -0
- package/framework/tasks/qa-planning-gate-define.md +339 -0
- package/framework/tasks/qa-planning-risk-matrix.md +631 -0
- package/framework/tasks/qa-planning-test-strategy.md +217 -0
- package/framework/tasks/tasks-acceptance-write.md +75 -0
- package/framework/tasks/tasks-consolidate.md +57 -0
- package/framework/tasks/tasks-decompose.md +80 -0
- package/framework/tasks/tasks-estimate.md +66 -0
- package/framework/tasks/ux-a11y-check.md +49 -0
- package/framework/tasks/ux-component-map.md +55 -0
- package/framework/tasks/ux-consolidate.md +46 -0
- package/framework/tasks/ux-user-flow.md +46 -0
- package/framework/tasks/ux-wireframe.md +76 -0
- package/package.json +1 -1
- package/scripts/bundle-framework.js +2 -0
- package/scripts/changelog-generator.js +222 -0
- package/scripts/codebase-mapper.js +728 -0
- package/scripts/commit-message-generator.js +167 -0
- package/scripts/coverage-analyzer.js +260 -0
- package/scripts/dependency-analyzer.js +280 -0
- package/scripts/framework-analyzer.js +308 -0
- package/scripts/generate-constitution-domain.js +253 -0
- package/scripts/health-check.js +481 -0
- package/scripts/ide-sync.js +327 -0
- package/scripts/performance-analyzer.js +325 -0
- package/scripts/plan-tracker.js +278 -0
- package/scripts/populate-entity-registry.js +481 -0
- package/scripts/pr-review.js +317 -0
- package/scripts/rollback-manager.js +310 -0
- package/scripts/stuck-detector.js +343 -0
- package/scripts/test-quality-assessment.js +257 -0
- package/scripts/validate-agents.js +367 -0
- package/scripts/validate-tasks.js +465 -0
- package/src/autonomy/autonomous-gate.js +293 -0
- package/src/autonomy/index.js +51 -0
- package/src/autonomy/mode-manager.js +225 -0
- package/src/autonomy/mode-suggester.js +283 -0
- package/src/autonomy/progress-reporter.js +268 -0
- package/src/autonomy/safety-net.js +320 -0
- package/src/context/bracket-tracker.js +79 -0
- package/src/context/domain-loader.js +107 -0
- package/src/context/engine.js +144 -0
- package/src/context/formatter.js +184 -0
- package/src/context/index.js +4 -0
- package/src/context/layers/l0-constitution.js +28 -0
- package/src/context/layers/l1-global.js +37 -0
- package/src/context/layers/l2-agent.js +39 -0
- package/src/context/layers/l3-workflow.js +42 -0
- package/src/context/layers/l4-task.js +24 -0
- package/src/decision/analyzer.js +167 -0
- package/src/decision/engine.js +270 -0
- package/src/decision/index.js +38 -0
- package/src/decision/registry-healer.js +450 -0
- package/src/decision/registry-updater.js +330 -0
- package/src/gates/circuit-breaker.js +119 -0
- package/src/gates/g1-planning-complete.js +153 -0
- package/src/gates/g2-qa-planning.js +153 -0
- package/src/gates/g3-implementation.js +188 -0
- package/src/gates/g4-qa-implementation.js +207 -0
- package/src/gates/g5-deploy-ready.js +180 -0
- package/src/gates/gate-base.js +144 -0
- package/src/gates/index.js +46 -0
- package/src/installer/brownfield-upgrader.js +249 -0
- package/src/installer/core.js +55 -3
- package/src/installer/file-hasher.js +51 -0
- package/src/installer/manifest.js +117 -0
- package/src/installer/templates.js +17 -15
- package/src/installer/transaction.js +229 -0
- package/src/installer/validator.js +18 -1
- package/src/memory/agent-memory.js +255 -0
- package/src/memory/gotchas-injector.js +72 -0
- package/src/memory/gotchas.js +361 -0
- package/src/memory/index.js +35 -0
- package/src/memory/search.js +233 -0
- package/src/memory/session-digest.js +239 -0
- package/src/merger/env-merger.js +112 -0
- package/src/merger/index.js +56 -0
- package/src/merger/replace-merger.js +51 -0
- package/src/merger/yaml-merger.js +127 -0
- package/src/orchestrator/agent-selector.js +285 -0
- package/src/orchestrator/deviation-handler.js +350 -0
- package/src/orchestrator/handoff-engine.js +271 -0
- package/src/orchestrator/index.js +67 -0
- package/src/orchestrator/intent-classifier.js +264 -0
- package/src/orchestrator/pipeline-manager.js +492 -0
- package/src/orchestrator/pipeline-state.js +223 -0
- package/src/orchestrator/session-manager.js +409 -0
- package/src/tasks/executor.js +195 -0
- package/src/tasks/handoff.js +226 -0
- package/src/tasks/index.js +4 -0
- package/src/tasks/loader.js +210 -0
- package/src/tasks/router.js +182 -0
- package/src/terminal/collector.js +216 -0
- package/src/terminal/index.js +30 -0
- package/src/terminal/isolation.js +129 -0
- package/src/terminal/monitor.js +277 -0
- package/src/terminal/spawner.js +269 -0
- package/src/upgrade/checker.js +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the path to an agent's memory file.
|
|
6
|
+
* @param {string} projectDir - Project directory
|
|
7
|
+
* @param {string} agentName - Agent name
|
|
8
|
+
* @returns {string} Path to MEMORY.md
|
|
9
|
+
*/
|
|
10
|
+
function getAgentMemoryPath(projectDir, agentName) {
|
|
11
|
+
return join(projectDir, '.chati', 'memories', agentName, 'MEMORY.md');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse markdown memory file into structured entries.
|
|
16
|
+
* @param {string} content - Markdown content
|
|
17
|
+
* @returns {object[]} Array of entries
|
|
18
|
+
*/
|
|
19
|
+
function parseMemoryMarkdown(content) {
|
|
20
|
+
const entries = [];
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
let currentCategory = null;
|
|
23
|
+
let currentEntry = null;
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
// Category header (## Category)
|
|
27
|
+
if (line.startsWith('## ')) {
|
|
28
|
+
if (currentEntry) {
|
|
29
|
+
entries.push(currentEntry);
|
|
30
|
+
}
|
|
31
|
+
currentCategory = line.substring(3).trim();
|
|
32
|
+
currentEntry = null;
|
|
33
|
+
}
|
|
34
|
+
// Entry item (- content)
|
|
35
|
+
else if (line.startsWith('- ') && currentCategory) {
|
|
36
|
+
if (currentEntry) {
|
|
37
|
+
entries.push(currentEntry);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = line.substring(2).trim();
|
|
41
|
+
|
|
42
|
+
// Parse tags [tag1, tag2]
|
|
43
|
+
const tagMatch = content.match(/\[([^\]]+)\]\s*$/);
|
|
44
|
+
const tags = tagMatch ? tagMatch[1].split(',').map(t => t.trim()) : [];
|
|
45
|
+
const cleanContent = tagMatch ? content.substring(0, tagMatch.index).trim() : content;
|
|
46
|
+
|
|
47
|
+
// Parse confidence (high/medium/low)
|
|
48
|
+
const confidenceMatch = cleanContent.match(/\((high|medium|low)\)\s*$/i);
|
|
49
|
+
const confidence = confidenceMatch ? confidenceMatch[1].toLowerCase() : 'medium';
|
|
50
|
+
const finalContent = confidenceMatch ? cleanContent.substring(0, confidenceMatch.index).trim() : cleanContent;
|
|
51
|
+
|
|
52
|
+
currentEntry = {
|
|
53
|
+
category: currentCategory,
|
|
54
|
+
content: finalContent,
|
|
55
|
+
confidence,
|
|
56
|
+
tags,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Continuation of entry
|
|
60
|
+
else if (line.trim().startsWith(' ') && currentEntry) {
|
|
61
|
+
currentEntry.content += '\n' + line.trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (currentEntry) {
|
|
66
|
+
entries.push(currentEntry);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return entries;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format entries back to markdown.
|
|
74
|
+
* @param {object[]} entries - Array of entry objects
|
|
75
|
+
* @returns {string} Markdown content
|
|
76
|
+
*/
|
|
77
|
+
function formatMemoryMarkdown(entries) {
|
|
78
|
+
const byCategory = {};
|
|
79
|
+
|
|
80
|
+
entries.forEach(entry => {
|
|
81
|
+
if (!byCategory[entry.category]) {
|
|
82
|
+
byCategory[entry.category] = [];
|
|
83
|
+
}
|
|
84
|
+
byCategory[entry.category].push(entry);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const sections = Object.entries(byCategory).map(([category, items]) => {
|
|
88
|
+
const itemsText = items.map(item => {
|
|
89
|
+
const confidenceText = item.confidence !== 'medium' ? ` (${item.confidence})` : '';
|
|
90
|
+
const tagsText = item.tags && item.tags.length > 0 ? ` [${item.tags.join(', ')}]` : '';
|
|
91
|
+
return `- ${item.content}${confidenceText}${tagsText}`;
|
|
92
|
+
}).join('\n');
|
|
93
|
+
|
|
94
|
+
return `## ${category}\n\n${itemsText}`;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return sections.join('\n\n') + '\n';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read an agent's memory file.
|
|
102
|
+
* @param {string} projectDir - Project directory
|
|
103
|
+
* @param {string} agentName - Agent name
|
|
104
|
+
* @returns {{ loaded: boolean, entries: object[], raw: string }}
|
|
105
|
+
*/
|
|
106
|
+
export function readAgentMemory(projectDir, agentName) {
|
|
107
|
+
const memoryPath = getAgentMemoryPath(projectDir, agentName);
|
|
108
|
+
|
|
109
|
+
if (!existsSync(memoryPath)) {
|
|
110
|
+
return {
|
|
111
|
+
loaded: false,
|
|
112
|
+
entries: [],
|
|
113
|
+
raw: '',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const raw = readFileSync(memoryPath, 'utf-8');
|
|
119
|
+
const entries = parseMemoryMarkdown(raw);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
loaded: true,
|
|
123
|
+
entries,
|
|
124
|
+
raw,
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
return {
|
|
128
|
+
loaded: false,
|
|
129
|
+
entries: [],
|
|
130
|
+
raw: '',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Write/append an entry to an agent's memory.
|
|
137
|
+
* @param {string} projectDir - Project directory
|
|
138
|
+
* @param {string} agentName - Agent name
|
|
139
|
+
* @param {object} entry - { category, content, confidence, tags }
|
|
140
|
+
* @returns {{ saved: boolean }}
|
|
141
|
+
*/
|
|
142
|
+
export function writeAgentMemory(projectDir, agentName, entry) {
|
|
143
|
+
const memoryPath = getAgentMemoryPath(projectDir, agentName);
|
|
144
|
+
const dir = dirname(memoryPath);
|
|
145
|
+
|
|
146
|
+
if (!existsSync(dir)) {
|
|
147
|
+
mkdirSync(dir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Read existing entries
|
|
151
|
+
const existing = readAgentMemory(projectDir, agentName);
|
|
152
|
+
|
|
153
|
+
// Add new entry with defaults
|
|
154
|
+
const newEntry = {
|
|
155
|
+
category: entry.category || 'General',
|
|
156
|
+
content: entry.content,
|
|
157
|
+
confidence: entry.confidence || 'medium',
|
|
158
|
+
tags: entry.tags || [],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
existing.entries.push(newEntry);
|
|
162
|
+
|
|
163
|
+
// Write back
|
|
164
|
+
const markdown = formatMemoryMarkdown(existing.entries);
|
|
165
|
+
writeFileSync(memoryPath, markdown, 'utf-8');
|
|
166
|
+
|
|
167
|
+
return { saved: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Search across all agent memories.
|
|
172
|
+
* @param {string} projectDir - Project directory
|
|
173
|
+
* @param {string} query - Search query
|
|
174
|
+
* @returns {object[]} Matching entries with agent attribution
|
|
175
|
+
*/
|
|
176
|
+
export function searchAgentMemories(projectDir, query) {
|
|
177
|
+
const memoriesDir = join(projectDir, '.chati', 'memories');
|
|
178
|
+
|
|
179
|
+
if (!existsSync(memoriesDir)) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const results = [];
|
|
184
|
+
const queryLower = query.toLowerCase();
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const agentDirs = readdirSync(memoriesDir, { withFileTypes: true })
|
|
188
|
+
.filter(d => d.isDirectory())
|
|
189
|
+
.map(d => d.name);
|
|
190
|
+
|
|
191
|
+
for (const agentName of agentDirs) {
|
|
192
|
+
const memory = readAgentMemory(projectDir, agentName);
|
|
193
|
+
|
|
194
|
+
if (!memory.loaded) continue;
|
|
195
|
+
|
|
196
|
+
memory.entries.forEach((entry, index) => {
|
|
197
|
+
const matchesContent = entry.content.toLowerCase().includes(queryLower);
|
|
198
|
+
const matchesCategory = entry.category.toLowerCase().includes(queryLower);
|
|
199
|
+
const matchesTags = entry.tags.some(t => t.toLowerCase().includes(queryLower));
|
|
200
|
+
|
|
201
|
+
if (matchesContent || matchesCategory || matchesTags) {
|
|
202
|
+
results.push({
|
|
203
|
+
agent: agentName,
|
|
204
|
+
...entry,
|
|
205
|
+
index,
|
|
206
|
+
matchType: matchesContent ? 'content' : matchesCategory ? 'category' : 'tag',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return results;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get memory stats per agent.
|
|
220
|
+
* @param {string} projectDir - Project directory
|
|
221
|
+
* @returns {object} { byAgent: { agent: { entries, lastUpdated } } }
|
|
222
|
+
*/
|
|
223
|
+
export function getAgentMemoryStats(projectDir) {
|
|
224
|
+
const memoriesDir = join(projectDir, '.chati', 'memories');
|
|
225
|
+
|
|
226
|
+
if (!existsSync(memoriesDir)) {
|
|
227
|
+
return { byAgent: {} };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const stats = { byAgent: {} };
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const agentDirs = readdirSync(memoriesDir, { withFileTypes: true })
|
|
234
|
+
.filter(d => d.isDirectory())
|
|
235
|
+
.map(d => d.name);
|
|
236
|
+
|
|
237
|
+
for (const agentName of agentDirs) {
|
|
238
|
+
const memoryPath = getAgentMemoryPath(projectDir, agentName);
|
|
239
|
+
|
|
240
|
+
if (!existsSync(memoryPath)) continue;
|
|
241
|
+
|
|
242
|
+
const memory = readAgentMemory(projectDir, agentName);
|
|
243
|
+
|
|
244
|
+
stats.byAgent[agentName] = {
|
|
245
|
+
entries: memory.entries.length,
|
|
246
|
+
lastUpdated: null, // Would need fs.statSync for actual timestamp
|
|
247
|
+
categories: [...new Set(memory.entries.map(e => e.category))],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
return { byAgent: {} };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return stats;
|
|
255
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getRelevantGotchas } from './gotchas.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a gotchas context block for injection into agent prompts.
|
|
5
|
+
* @param {string} projectDir - Project directory
|
|
6
|
+
* @param {object} context - { agent, task, keywords }
|
|
7
|
+
* @returns {string} Formatted XML block with relevant gotchas
|
|
8
|
+
*/
|
|
9
|
+
export function buildGotchasContext(projectDir, context) {
|
|
10
|
+
const relevantGotchas = getRelevantGotchas(projectDir, context);
|
|
11
|
+
|
|
12
|
+
if (relevantGotchas.length === 0) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Take top 5 most relevant
|
|
17
|
+
const topGotchas = relevantGotchas.slice(0, 5);
|
|
18
|
+
|
|
19
|
+
const gotchasXml = topGotchas.map(gotcha => {
|
|
20
|
+
const description = gotcha.resolution
|
|
21
|
+
? `${gotcha.original_message} — Resolution: ${gotcha.resolution}`
|
|
22
|
+
: gotcha.original_message;
|
|
23
|
+
|
|
24
|
+
return ` <gotcha id="${gotcha.id}" pattern="${gotcha.pattern}" count="${gotcha.count}" relevance="${gotcha.relevance}">
|
|
25
|
+
${escapeXml(description)}
|
|
26
|
+
</gotcha>`;
|
|
27
|
+
}).join('\n');
|
|
28
|
+
|
|
29
|
+
return `<gotchas agent="${escapeXml(context.agent || 'unknown')}" count="${topGotchas.length}">
|
|
30
|
+
${gotchasXml}
|
|
31
|
+
</gotchas>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Escape XML special characters.
|
|
36
|
+
* @param {string} text - Text to escape
|
|
37
|
+
* @returns {string} Escaped text
|
|
38
|
+
*/
|
|
39
|
+
function escapeXml(text) {
|
|
40
|
+
if (typeof text !== 'string') {
|
|
41
|
+
return String(text);
|
|
42
|
+
}
|
|
43
|
+
return text
|
|
44
|
+
.replace(/&/g, '&')
|
|
45
|
+
.replace(/</g, '<')
|
|
46
|
+
.replace(/>/g, '>')
|
|
47
|
+
.replace(/"/g, '"')
|
|
48
|
+
.replace(/'/g, ''');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a compact gotchas summary for smaller context windows.
|
|
53
|
+
* @param {string} projectDir - Project directory
|
|
54
|
+
* @param {object} context - { agent, task, keywords }
|
|
55
|
+
* @returns {string} Compact text summary
|
|
56
|
+
*/
|
|
57
|
+
export function buildCompactGotchasSummary(projectDir, context) {
|
|
58
|
+
const relevantGotchas = getRelevantGotchas(projectDir, context);
|
|
59
|
+
|
|
60
|
+
if (relevantGotchas.length === 0) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const topGotchas = relevantGotchas.slice(0, 3);
|
|
65
|
+
|
|
66
|
+
const lines = topGotchas.map((gotcha, idx) => {
|
|
67
|
+
const resolution = gotcha.resolution ? ` → ${gotcha.resolution}` : '';
|
|
68
|
+
return `${idx + 1}. [${gotcha.id}] ${gotcha.message} (seen ${gotcha.count}x)${resolution}`;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return `⚠️ Gotchas (${context.agent || 'unknown'}):\n${lines.join('\n')}`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
const GOTCHAS_FILE = '.chati/memories/shared/gotchas.json';
|
|
6
|
+
const ERROR_LOG_FILE = '.chati/memories/shared/error-log.json';
|
|
7
|
+
const ERROR_PATTERN_THRESHOLD = 3; // Promote after 3 occurrences
|
|
8
|
+
const ERROR_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
9
|
+
const ERROR_RETENTION_DAYS = 7;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize error message to detect patterns.
|
|
13
|
+
* Strips numbers, file paths, and specific identifiers.
|
|
14
|
+
* @param {string} message - Error message
|
|
15
|
+
* @returns {string} Normalized message
|
|
16
|
+
*/
|
|
17
|
+
function normalizeErrorMessage(message) {
|
|
18
|
+
return message
|
|
19
|
+
.replace(/\d+/g, 'N') // Replace numbers with N
|
|
20
|
+
.replace(/\/[^\s]+/g, '/PATH') // Replace paths
|
|
21
|
+
.replace(/\b[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\b/gi, 'UUID') // UUIDs
|
|
22
|
+
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, 'EMAIL') // Emails
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hash an error message to create a pattern identifier.
|
|
29
|
+
* @param {string} message - Error message
|
|
30
|
+
* @returns {string} Hash (first 8 chars)
|
|
31
|
+
*/
|
|
32
|
+
function hashErrorMessage(message) {
|
|
33
|
+
const normalized = normalizeErrorMessage(message);
|
|
34
|
+
return createHash('md5').update(normalized).digest('hex').substring(0, 8);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load gotchas from disk.
|
|
39
|
+
* @param {string} projectDir - Project directory
|
|
40
|
+
* @returns {object[]} Array of gotcha objects
|
|
41
|
+
*/
|
|
42
|
+
function loadGotchas(projectDir) {
|
|
43
|
+
const gotchasPath = join(projectDir, GOTCHAS_FILE);
|
|
44
|
+
if (!existsSync(gotchasPath)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(gotchasPath, 'utf-8');
|
|
49
|
+
return JSON.parse(content);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save gotchas to disk.
|
|
57
|
+
* @param {string} projectDir - Project directory
|
|
58
|
+
* @param {object[]} gotchas - Array of gotcha objects
|
|
59
|
+
*/
|
|
60
|
+
function saveGotchas(projectDir, gotchas) {
|
|
61
|
+
const gotchasPath = join(projectDir, GOTCHAS_FILE);
|
|
62
|
+
const dir = dirname(gotchasPath);
|
|
63
|
+
if (!existsSync(dir)) {
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(gotchasPath, JSON.stringify(gotchas, null, 2), 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load error log from disk.
|
|
71
|
+
* @param {string} projectDir - Project directory
|
|
72
|
+
* @returns {object[]} Array of error log entries
|
|
73
|
+
*/
|
|
74
|
+
function loadErrorLog(projectDir) {
|
|
75
|
+
const errorLogPath = join(projectDir, ERROR_LOG_FILE);
|
|
76
|
+
if (!existsSync(errorLogPath)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const content = readFileSync(errorLogPath, 'utf-8');
|
|
81
|
+
return JSON.parse(content);
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Save error log to disk.
|
|
89
|
+
* @param {string} projectDir - Project directory
|
|
90
|
+
* @param {object[]} errorLog - Array of error log entries
|
|
91
|
+
*/
|
|
92
|
+
function saveErrorLog(projectDir, errorLog) {
|
|
93
|
+
const errorLogPath = join(projectDir, ERROR_LOG_FILE);
|
|
94
|
+
const dir = dirname(errorLogPath);
|
|
95
|
+
if (!existsSync(dir)) {
|
|
96
|
+
mkdirSync(dir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
writeFileSync(errorLogPath, JSON.stringify(errorLog, null, 2), 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate a unique gotcha ID.
|
|
103
|
+
* @param {object[]} gotchas - Existing gotchas
|
|
104
|
+
* @returns {string} New ID in format G001, G002, etc.
|
|
105
|
+
*/
|
|
106
|
+
function generateGotchaId(gotchas) {
|
|
107
|
+
const maxId = gotchas.reduce((max, g) => {
|
|
108
|
+
const num = parseInt(g.id.substring(1), 10);
|
|
109
|
+
return num > max ? num : max;
|
|
110
|
+
}, 0);
|
|
111
|
+
return `G${String(maxId + 1).padStart(3, '0')}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Record an error occurrence. If this error has appeared 3+ times in 24h, promote to gotcha.
|
|
116
|
+
* @param {string} projectDir - Project directory
|
|
117
|
+
* @param {object} error - { message, agent, task, context }
|
|
118
|
+
* @returns {{ recorded: boolean, promoted: boolean, gotcha: object|null }}
|
|
119
|
+
*/
|
|
120
|
+
export function recordError(projectDir, error) {
|
|
121
|
+
const { message, agent, task, context = {} } = error;
|
|
122
|
+
const hash = hashErrorMessage(message);
|
|
123
|
+
const timestamp = new Date().toISOString();
|
|
124
|
+
|
|
125
|
+
// Load error log
|
|
126
|
+
const errorLog = loadErrorLog(projectDir);
|
|
127
|
+
|
|
128
|
+
// Add new error entry
|
|
129
|
+
errorLog.push({
|
|
130
|
+
message,
|
|
131
|
+
agent,
|
|
132
|
+
task,
|
|
133
|
+
timestamp,
|
|
134
|
+
hash,
|
|
135
|
+
context,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Save error log
|
|
139
|
+
saveErrorLog(projectDir, errorLog);
|
|
140
|
+
|
|
141
|
+
// Check if this pattern should be promoted
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const recentErrors = errorLog.filter(e => {
|
|
144
|
+
const errorTime = new Date(e.timestamp).getTime();
|
|
145
|
+
return e.hash === hash && (now - errorTime) <= ERROR_WINDOW_MS;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (recentErrors.length >= ERROR_PATTERN_THRESHOLD) {
|
|
149
|
+
// Promote to gotcha
|
|
150
|
+
const gotchas = loadGotchas(projectDir);
|
|
151
|
+
|
|
152
|
+
// Check if already exists
|
|
153
|
+
const existingGotcha = gotchas.find(g => g.pattern === hash);
|
|
154
|
+
|
|
155
|
+
if (existingGotcha) {
|
|
156
|
+
// Update existing gotcha
|
|
157
|
+
existingGotcha.count = recentErrors.length;
|
|
158
|
+
existingGotcha.last_seen = timestamp;
|
|
159
|
+
saveGotchas(projectDir, gotchas);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
recorded: true,
|
|
163
|
+
promoted: false,
|
|
164
|
+
gotcha: existingGotcha,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Create new gotcha
|
|
169
|
+
const gotchaId = generateGotchaId(gotchas);
|
|
170
|
+
const newGotcha = {
|
|
171
|
+
id: gotchaId,
|
|
172
|
+
pattern: hash,
|
|
173
|
+
message: normalizeErrorMessage(message),
|
|
174
|
+
original_message: message,
|
|
175
|
+
agent,
|
|
176
|
+
task,
|
|
177
|
+
count: recentErrors.length,
|
|
178
|
+
first_seen: recentErrors[0].timestamp,
|
|
179
|
+
last_seen: timestamp,
|
|
180
|
+
promoted_at: timestamp,
|
|
181
|
+
resolution: null,
|
|
182
|
+
context,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
gotchas.push(newGotcha);
|
|
186
|
+
saveGotchas(projectDir, gotchas);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
recorded: true,
|
|
190
|
+
promoted: true,
|
|
191
|
+
gotcha: newGotcha,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
recorded: true,
|
|
197
|
+
promoted: false,
|
|
198
|
+
gotcha: null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get all gotchas (promoted errors).
|
|
204
|
+
* @param {string} projectDir - Project directory
|
|
205
|
+
* @returns {object[]} Array of gotcha objects
|
|
206
|
+
*/
|
|
207
|
+
export function getGotchas(projectDir) {
|
|
208
|
+
return loadGotchas(projectDir);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Calculate relevance score for a gotcha given a context.
|
|
213
|
+
* @param {object} gotcha - Gotcha object
|
|
214
|
+
* @param {object} context - { agent, task, keywords }
|
|
215
|
+
* @returns {number} Relevance score (0-100)
|
|
216
|
+
*/
|
|
217
|
+
function calculateRelevance(gotcha, context) {
|
|
218
|
+
let score = 0;
|
|
219
|
+
|
|
220
|
+
// Agent match (30 points)
|
|
221
|
+
if (context.agent && gotcha.agent === context.agent) {
|
|
222
|
+
score += 30;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Task match (20 points)
|
|
226
|
+
if (context.task && gotcha.task === context.task) {
|
|
227
|
+
score += 20;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Keyword matches (50 points total)
|
|
231
|
+
if (context.keywords && Array.isArray(context.keywords)) {
|
|
232
|
+
const gotchaText = `${gotcha.message} ${gotcha.original_message || ''} ${JSON.stringify(gotcha.context || {})}`.toLowerCase();
|
|
233
|
+
const matchedKeywords = context.keywords.filter(kw =>
|
|
234
|
+
gotchaText.includes(kw.toLowerCase())
|
|
235
|
+
);
|
|
236
|
+
const keywordScore = Math.min(50, (matchedKeywords.length / context.keywords.length) * 50);
|
|
237
|
+
score += keywordScore;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Only add bonuses if there's some base relevance
|
|
241
|
+
if (score > 0) {
|
|
242
|
+
// Recency bonus (up to 10 points)
|
|
243
|
+
const daysSinceLastSeen = (Date.now() - new Date(gotcha.last_seen).getTime()) / (1000 * 60 * 60 * 24);
|
|
244
|
+
if (daysSinceLastSeen < 7) {
|
|
245
|
+
score += 10 * (1 - daysSinceLastSeen / 7);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Frequency bonus (up to 10 points)
|
|
249
|
+
const frequencyBonus = Math.min(10, gotcha.count * 2);
|
|
250
|
+
score += frequencyBonus;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return Math.round(score);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get relevant gotchas for a given agent/task context.
|
|
258
|
+
* Matches by agent, task, and error pattern similarity.
|
|
259
|
+
* @param {string} projectDir - Project directory
|
|
260
|
+
* @param {object} context - { agent, task, keywords }
|
|
261
|
+
* @returns {object[]} Relevant gotchas sorted by relevance
|
|
262
|
+
*/
|
|
263
|
+
export function getRelevantGotchas(projectDir, context) {
|
|
264
|
+
const gotchas = loadGotchas(projectDir);
|
|
265
|
+
|
|
266
|
+
const scored = gotchas.map(gotcha => ({
|
|
267
|
+
...gotcha,
|
|
268
|
+
relevance: calculateRelevance(gotcha, context),
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
return scored
|
|
272
|
+
.filter(g => g.relevance > 0)
|
|
273
|
+
.sort((a, b) => b.relevance - a.relevance);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get error tracking statistics.
|
|
278
|
+
* @param {string} projectDir - Project directory
|
|
279
|
+
* @returns {{ totalErrors: number, totalGotchas: number, recentErrors: number, topPatterns: object[] }}
|
|
280
|
+
*/
|
|
281
|
+
export function getGotchaStats(projectDir) {
|
|
282
|
+
const gotchas = loadGotchas(projectDir);
|
|
283
|
+
const errorLog = loadErrorLog(projectDir);
|
|
284
|
+
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
const recentErrors = errorLog.filter(e => {
|
|
287
|
+
const errorTime = new Date(e.timestamp).getTime();
|
|
288
|
+
return (now - errorTime) <= ERROR_WINDOW_MS;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Count errors by pattern
|
|
292
|
+
const patternCounts = {};
|
|
293
|
+
errorLog.forEach(e => {
|
|
294
|
+
patternCounts[e.hash] = (patternCounts[e.hash] || 0) + 1;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const topPatterns = Object.entries(patternCounts)
|
|
298
|
+
.map(([hash, count]) => {
|
|
299
|
+
const gotcha = gotchas.find(g => g.pattern === hash);
|
|
300
|
+
return {
|
|
301
|
+
hash,
|
|
302
|
+
count,
|
|
303
|
+
message: gotcha ? gotcha.message : 'Unknown pattern',
|
|
304
|
+
promoted: !!gotcha,
|
|
305
|
+
};
|
|
306
|
+
})
|
|
307
|
+
.sort((a, b) => b.count - a.count)
|
|
308
|
+
.slice(0, 10);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
totalErrors: errorLog.length,
|
|
312
|
+
totalGotchas: gotchas.length,
|
|
313
|
+
recentErrors: recentErrors.length,
|
|
314
|
+
topPatterns,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Clear expired error entries (older than 7 days).
|
|
320
|
+
* @param {string} projectDir - Project directory
|
|
321
|
+
* @returns {{ cleared: number }}
|
|
322
|
+
*/
|
|
323
|
+
export function clearExpiredErrors(projectDir) {
|
|
324
|
+
const errorLog = loadErrorLog(projectDir);
|
|
325
|
+
const retentionMs = ERROR_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
326
|
+
const now = Date.now();
|
|
327
|
+
|
|
328
|
+
const originalCount = errorLog.length;
|
|
329
|
+
const filtered = errorLog.filter(e => {
|
|
330
|
+
const errorTime = new Date(e.timestamp).getTime();
|
|
331
|
+
return (now - errorTime) <= retentionMs;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
saveErrorLog(projectDir, filtered);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
cleared: originalCount - filtered.length,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Update gotcha resolution.
|
|
343
|
+
* @param {string} projectDir - Project directory
|
|
344
|
+
* @param {string} gotchaId - Gotcha ID
|
|
345
|
+
* @param {string} resolution - Resolution description
|
|
346
|
+
* @returns {{ updated: boolean }}
|
|
347
|
+
*/
|
|
348
|
+
export function updateGotchaResolution(projectDir, gotchaId, resolution) {
|
|
349
|
+
const gotchas = loadGotchas(projectDir);
|
|
350
|
+
const gotcha = gotchas.find(g => g.id === gotchaId);
|
|
351
|
+
|
|
352
|
+
if (!gotcha) {
|
|
353
|
+
return { updated: false };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
gotcha.resolution = resolution;
|
|
357
|
+
gotcha.resolved_at = new Date().toISOString();
|
|
358
|
+
saveGotchas(projectDir, gotchas);
|
|
359
|
+
|
|
360
|
+
return { updated: true };
|
|
361
|
+
}
|