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.
Files changed (200) hide show
  1. package/README.md +3 -3
  2. package/framework/agents/build/dev.md +343 -0
  3. package/framework/agents/clarity/architect.md +112 -0
  4. package/framework/agents/clarity/brief.md +182 -0
  5. package/framework/agents/clarity/brownfield-wu.md +181 -0
  6. package/framework/agents/clarity/detail.md +110 -0
  7. package/framework/agents/clarity/greenfield-wu.md +153 -0
  8. package/framework/agents/clarity/ux.md +112 -0
  9. package/framework/config.yaml +3 -3
  10. package/framework/constitution.md +31 -1
  11. package/framework/context/governance.md +37 -0
  12. package/framework/context/protocols.md +34 -0
  13. package/framework/context/quality.md +27 -0
  14. package/framework/context/root.md +24 -0
  15. package/framework/domains/agents/architect.yaml +51 -0
  16. package/framework/domains/agents/brief.yaml +47 -0
  17. package/framework/domains/agents/brownfield-wu.yaml +49 -0
  18. package/framework/domains/agents/detail.yaml +47 -0
  19. package/framework/domains/agents/dev.yaml +49 -0
  20. package/framework/domains/agents/devops.yaml +43 -0
  21. package/framework/domains/agents/greenfield-wu.yaml +47 -0
  22. package/framework/domains/agents/orchestrator.yaml +49 -0
  23. package/framework/domains/agents/phases.yaml +47 -0
  24. package/framework/domains/agents/qa-implementation.yaml +43 -0
  25. package/framework/domains/agents/qa-planning.yaml +44 -0
  26. package/framework/domains/agents/tasks.yaml +48 -0
  27. package/framework/domains/agents/ux.yaml +50 -0
  28. package/framework/domains/constitution.yaml +77 -0
  29. package/framework/domains/global.yaml +64 -0
  30. package/framework/domains/workflows/brownfield-discovery.yaml +16 -0
  31. package/framework/domains/workflows/brownfield-fullstack.yaml +26 -0
  32. package/framework/domains/workflows/brownfield-service.yaml +22 -0
  33. package/framework/domains/workflows/brownfield-ui.yaml +22 -0
  34. package/framework/domains/workflows/greenfield-fullstack.yaml +26 -0
  35. package/framework/hooks/constitution-guard.js +101 -0
  36. package/framework/hooks/mode-governance.js +92 -0
  37. package/framework/hooks/model-governance.js +76 -0
  38. package/framework/hooks/prism-engine.js +89 -0
  39. package/framework/hooks/session-digest.js +60 -0
  40. package/framework/hooks/settings.json +44 -0
  41. package/framework/migrations/v1.4-to-v2.0.yaml +167 -0
  42. package/framework/migrations/v2.0-to-v2.0.1.yaml +132 -0
  43. package/framework/orchestrator/chati.md +284 -6
  44. package/framework/tasks/architect-api-design.md +63 -0
  45. package/framework/tasks/architect-consolidate.md +47 -0
  46. package/framework/tasks/architect-db-design.md +73 -0
  47. package/framework/tasks/architect-design.md +95 -0
  48. package/framework/tasks/architect-security-review.md +62 -0
  49. package/framework/tasks/architect-stack-selection.md +53 -0
  50. package/framework/tasks/brief-consolidate.md +249 -0
  51. package/framework/tasks/brief-constraint-identify.md +277 -0
  52. package/framework/tasks/brief-extract-requirements.md +339 -0
  53. package/framework/tasks/brief-stakeholder-map.md +176 -0
  54. package/framework/tasks/brief-validate-completeness.md +121 -0
  55. package/framework/tasks/brownfield-wu-architecture-map.md +394 -0
  56. package/framework/tasks/brownfield-wu-deep-discovery.md +312 -0
  57. package/framework/tasks/brownfield-wu-dependency-scan.md +359 -0
  58. package/framework/tasks/brownfield-wu-migration-plan.md +483 -0
  59. package/framework/tasks/brownfield-wu-report.md +325 -0
  60. package/framework/tasks/brownfield-wu-risk-assess.md +424 -0
  61. package/framework/tasks/detail-acceptance-criteria.md +372 -0
  62. package/framework/tasks/detail-consolidate.md +138 -0
  63. package/framework/tasks/detail-edge-case-analysis.md +300 -0
  64. package/framework/tasks/detail-expand-prd.md +389 -0
  65. package/framework/tasks/detail-nfr-extraction.md +223 -0
  66. package/framework/tasks/dev-code-review.md +404 -0
  67. package/framework/tasks/dev-consolidate.md +543 -0
  68. package/framework/tasks/dev-debug.md +322 -0
  69. package/framework/tasks/dev-implement.md +252 -0
  70. package/framework/tasks/dev-iterate.md +411 -0
  71. package/framework/tasks/dev-pr-prepare.md +497 -0
  72. package/framework/tasks/dev-refactor.md +342 -0
  73. package/framework/tasks/dev-test-write.md +306 -0
  74. package/framework/tasks/devops-ci-setup.md +412 -0
  75. package/framework/tasks/devops-consolidate.md +712 -0
  76. package/framework/tasks/devops-deploy-config.md +598 -0
  77. package/framework/tasks/devops-monitoring-setup.md +658 -0
  78. package/framework/tasks/devops-release-prepare.md +673 -0
  79. package/framework/tasks/greenfield-wu-analyze-empty.md +169 -0
  80. package/framework/tasks/greenfield-wu-report.md +266 -0
  81. package/framework/tasks/greenfield-wu-scaffold-detection.md +203 -0
  82. package/framework/tasks/greenfield-wu-tech-stack-assess.md +255 -0
  83. package/framework/tasks/orchestrator-deviation.md +260 -0
  84. package/framework/tasks/orchestrator-escalate.md +276 -0
  85. package/framework/tasks/orchestrator-handoff.md +243 -0
  86. package/framework/tasks/orchestrator-health.md +372 -0
  87. package/framework/tasks/orchestrator-mode-switch.md +262 -0
  88. package/framework/tasks/orchestrator-resume.md +189 -0
  89. package/framework/tasks/orchestrator-route.md +169 -0
  90. package/framework/tasks/orchestrator-spawn-terminal.md +358 -0
  91. package/framework/tasks/orchestrator-status.md +260 -0
  92. package/framework/tasks/orchestrator-suggest-mode.md +372 -0
  93. package/framework/tasks/phases-breakdown.md +91 -0
  94. package/framework/tasks/phases-dependency-mapping.md +67 -0
  95. package/framework/tasks/phases-mvp-scoping.md +94 -0
  96. package/framework/tasks/qa-impl-consolidate.md +522 -0
  97. package/framework/tasks/qa-impl-performance-test.md +487 -0
  98. package/framework/tasks/qa-impl-regression-check.md +413 -0
  99. package/framework/tasks/qa-impl-sast-scan.md +402 -0
  100. package/framework/tasks/qa-impl-test-execute.md +344 -0
  101. package/framework/tasks/qa-impl-verdict.md +339 -0
  102. package/framework/tasks/qa-planning-consolidate.md +309 -0
  103. package/framework/tasks/qa-planning-coverage-plan.md +338 -0
  104. package/framework/tasks/qa-planning-gate-define.md +339 -0
  105. package/framework/tasks/qa-planning-risk-matrix.md +631 -0
  106. package/framework/tasks/qa-planning-test-strategy.md +217 -0
  107. package/framework/tasks/tasks-acceptance-write.md +75 -0
  108. package/framework/tasks/tasks-consolidate.md +57 -0
  109. package/framework/tasks/tasks-decompose.md +80 -0
  110. package/framework/tasks/tasks-estimate.md +66 -0
  111. package/framework/tasks/ux-a11y-check.md +49 -0
  112. package/framework/tasks/ux-component-map.md +55 -0
  113. package/framework/tasks/ux-consolidate.md +46 -0
  114. package/framework/tasks/ux-user-flow.md +46 -0
  115. package/framework/tasks/ux-wireframe.md +76 -0
  116. package/package.json +1 -1
  117. package/scripts/bundle-framework.js +2 -0
  118. package/scripts/changelog-generator.js +222 -0
  119. package/scripts/codebase-mapper.js +728 -0
  120. package/scripts/commit-message-generator.js +167 -0
  121. package/scripts/coverage-analyzer.js +260 -0
  122. package/scripts/dependency-analyzer.js +280 -0
  123. package/scripts/framework-analyzer.js +308 -0
  124. package/scripts/generate-constitution-domain.js +253 -0
  125. package/scripts/health-check.js +481 -0
  126. package/scripts/ide-sync.js +327 -0
  127. package/scripts/performance-analyzer.js +325 -0
  128. package/scripts/plan-tracker.js +278 -0
  129. package/scripts/populate-entity-registry.js +481 -0
  130. package/scripts/pr-review.js +317 -0
  131. package/scripts/rollback-manager.js +310 -0
  132. package/scripts/stuck-detector.js +343 -0
  133. package/scripts/test-quality-assessment.js +257 -0
  134. package/scripts/validate-agents.js +367 -0
  135. package/scripts/validate-tasks.js +465 -0
  136. package/src/autonomy/autonomous-gate.js +293 -0
  137. package/src/autonomy/index.js +51 -0
  138. package/src/autonomy/mode-manager.js +225 -0
  139. package/src/autonomy/mode-suggester.js +283 -0
  140. package/src/autonomy/progress-reporter.js +268 -0
  141. package/src/autonomy/safety-net.js +320 -0
  142. package/src/context/bracket-tracker.js +79 -0
  143. package/src/context/domain-loader.js +107 -0
  144. package/src/context/engine.js +144 -0
  145. package/src/context/formatter.js +184 -0
  146. package/src/context/index.js +4 -0
  147. package/src/context/layers/l0-constitution.js +28 -0
  148. package/src/context/layers/l1-global.js +37 -0
  149. package/src/context/layers/l2-agent.js +39 -0
  150. package/src/context/layers/l3-workflow.js +42 -0
  151. package/src/context/layers/l4-task.js +24 -0
  152. package/src/decision/analyzer.js +167 -0
  153. package/src/decision/engine.js +270 -0
  154. package/src/decision/index.js +38 -0
  155. package/src/decision/registry-healer.js +450 -0
  156. package/src/decision/registry-updater.js +330 -0
  157. package/src/gates/circuit-breaker.js +119 -0
  158. package/src/gates/g1-planning-complete.js +153 -0
  159. package/src/gates/g2-qa-planning.js +153 -0
  160. package/src/gates/g3-implementation.js +188 -0
  161. package/src/gates/g4-qa-implementation.js +207 -0
  162. package/src/gates/g5-deploy-ready.js +180 -0
  163. package/src/gates/gate-base.js +144 -0
  164. package/src/gates/index.js +46 -0
  165. package/src/installer/brownfield-upgrader.js +249 -0
  166. package/src/installer/core.js +55 -3
  167. package/src/installer/file-hasher.js +51 -0
  168. package/src/installer/manifest.js +117 -0
  169. package/src/installer/templates.js +17 -15
  170. package/src/installer/transaction.js +229 -0
  171. package/src/installer/validator.js +18 -1
  172. package/src/memory/agent-memory.js +255 -0
  173. package/src/memory/gotchas-injector.js +72 -0
  174. package/src/memory/gotchas.js +361 -0
  175. package/src/memory/index.js +35 -0
  176. package/src/memory/search.js +233 -0
  177. package/src/memory/session-digest.js +239 -0
  178. package/src/merger/env-merger.js +112 -0
  179. package/src/merger/index.js +56 -0
  180. package/src/merger/replace-merger.js +51 -0
  181. package/src/merger/yaml-merger.js +127 -0
  182. package/src/orchestrator/agent-selector.js +285 -0
  183. package/src/orchestrator/deviation-handler.js +350 -0
  184. package/src/orchestrator/handoff-engine.js +271 -0
  185. package/src/orchestrator/index.js +67 -0
  186. package/src/orchestrator/intent-classifier.js +264 -0
  187. package/src/orchestrator/pipeline-manager.js +492 -0
  188. package/src/orchestrator/pipeline-state.js +223 -0
  189. package/src/orchestrator/session-manager.js +409 -0
  190. package/src/tasks/executor.js +195 -0
  191. package/src/tasks/handoff.js +226 -0
  192. package/src/tasks/index.js +4 -0
  193. package/src/tasks/loader.js +210 -0
  194. package/src/tasks/router.js +182 -0
  195. package/src/terminal/collector.js +216 -0
  196. package/src/terminal/index.js +30 -0
  197. package/src/terminal/isolation.js +129 -0
  198. package/src/terminal/monitor.js +277 -0
  199. package/src/terminal/spawner.js +269 -0
  200. 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, '&amp;')
45
+ .replace(/</g, '&lt;')
46
+ .replace(/>/g, '&gt;')
47
+ .replace(/"/g, '&quot;')
48
+ .replace(/'/g, '&apos;');
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
+ }