chati-dev 3.2.5 → 3.3.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 (98) hide show
  1. package/LICENSE +96 -0
  2. package/bin/chati.js +46 -0
  3. package/framework/agents/build/dev.md +122 -1
  4. package/framework/agents/deploy/devops.md +128 -3
  5. package/framework/agents/discover/brief.md +77 -15
  6. package/framework/agents/discover/brownfield-wu.md +2 -2
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +3 -3
  10. package/framework/agents/plan/phases.md +127 -2
  11. package/framework/agents/plan/tasks.md +127 -2
  12. package/framework/agents/plan/ux.md +269 -22
  13. package/framework/agents/quality/qa-implementation.md +172 -8
  14. package/framework/agents/quality/qa-planning.md +147 -2
  15. package/framework/config.yaml +9 -5
  16. package/framework/constitution.md +7 -1
  17. package/framework/context/quality.md +1 -1
  18. package/framework/context/root.md +1 -1
  19. package/framework/hooks/constitution-guard.js +18 -2
  20. package/framework/hooks/mode-governance.js +3 -3
  21. package/framework/hooks/read-protection.js +10 -2
  22. package/framework/i18n/en.yaml +6 -0
  23. package/framework/i18n/es.yaml +6 -0
  24. package/framework/i18n/fr.yaml +6 -0
  25. package/framework/i18n/pt.yaml +6 -0
  26. package/framework/orchestrator/chati.md +102 -6
  27. package/framework/schemas/task.schema.json +1 -1
  28. package/framework/tasks/architect-dep-audit.md +128 -0
  29. package/framework/tasks/architect-stack-selection.md +28 -0
  30. package/framework/workflows/brownfield-fullstack.yaml +2 -2
  31. package/framework/workflows/brownfield-service.yaml +2 -2
  32. package/framework/workflows/brownfield-ui.yaml +2 -2
  33. package/framework/workflows/greenfield-fullstack.yaml +6 -2
  34. package/framework/workflows/quick-flow.yaml +7 -5
  35. package/framework/workflows/standard-flow.yaml +171 -0
  36. package/package.json +4 -2
  37. package/src/api/index.js +129 -0
  38. package/src/autonomy/build-loop.js +93 -6
  39. package/src/autonomy/build-state.js +20 -2
  40. package/src/autonomy/cause-analyzer.js +177 -0
  41. package/src/autonomy/escalation.js +214 -0
  42. package/src/autonomy/safety-net.js +23 -5
  43. package/src/autonomy/worktree-manager.js +245 -0
  44. package/src/config/agent-customizer.js +227 -0
  45. package/src/config/ide-configs.js +57 -27
  46. package/src/decision/analyzer.js +148 -0
  47. package/src/decision/registry-healer.js +38 -21
  48. package/src/extensions/loader.js +151 -0
  49. package/src/extensions/registry.js +134 -0
  50. package/src/gates/circuit-breaker.js +32 -0
  51. package/src/gates/g3-implementation.js +30 -4
  52. package/src/gates/g4-qa-implementation.js +34 -5
  53. package/src/gates/gate-base.js +9 -0
  54. package/src/health/auto-fix.js +216 -0
  55. package/src/installer/core.js +24 -11
  56. package/src/installer/provider-overlay.js +82 -0
  57. package/src/installer/templates.js +22 -10
  58. package/src/installer/transaction.js +3 -2
  59. package/src/installer/validator.js +74 -0
  60. package/src/intelligence/context-status.js +9 -5
  61. package/src/intelligence/document-sharder.js +221 -0
  62. package/src/intelligence/elicitation.js +265 -0
  63. package/src/intelligence/timeline.js +5 -0
  64. package/src/memory/gotchas.js +78 -2
  65. package/src/merger/semantic-merger.js +292 -0
  66. package/src/orchestrator/agent-selector.js +20 -0
  67. package/src/orchestrator/handoff-engine.js +77 -0
  68. package/src/orchestrator/index.js +0 -8
  69. package/src/orchestrator/intent-classifier.js +182 -0
  70. package/src/orchestrator/pipeline-manager.js +125 -1
  71. package/src/orchestrator/session-manager.js +164 -2
  72. package/src/quality/metrics-collector.js +283 -0
  73. package/src/quality/test-runner.js +368 -0
  74. package/src/telemetry/collector.js +83 -0
  75. package/src/telemetry/config.js +119 -0
  76. package/src/telemetry/index.js +11 -0
  77. package/src/telemetry/schema.js +104 -0
  78. package/src/telemetry/sender.js +60 -0
  79. package/src/terminal/cli-registry.js +7 -1
  80. package/src/terminal/cost-tracker.js +197 -0
  81. package/src/terminal/handoff-parser.js +61 -4
  82. package/src/terminal/prompt-builder.js +56 -18
  83. package/src/terminal/rate-limiter.js +172 -0
  84. package/src/terminal/run-agent.js +39 -0
  85. package/src/terminal/run-parallel.js +22 -1
  86. package/src/terminal/spawner.js +181 -3
  87. package/src/upgrade/migrator.js +2 -2
  88. package/src/utils/event-bus.js +126 -0
  89. package/src/utils/file-lock.js +291 -0
  90. package/src/utils/schema-validator.js +226 -0
  91. package/src/wizard/i18n.js +11 -0
  92. package/src/wizard/index.js +42 -20
  93. package/src/wizard/questions.js +200 -39
  94. package/src/autonomy/execution-profile.js +0 -151
  95. package/src/intelligence/file-tracker.js +0 -117
  96. package/src/memory/gotchas-auto-capture.js +0 -253
  97. package/src/orchestrator/pipeline-state.js +0 -223
  98. package/src/terminal/wave-analyzer.js +0 -143
@@ -1,151 +0,0 @@
1
- /**
2
- * @fileoverview Execution profile manager.
3
- *
4
- * Manages 3 execution profiles that govern confirmation requirements:
5
- * - explore: read-only, no writes
6
- * - guided: confirm before writes (default)
7
- * - autonomous: full autonomy with quality gates
8
- *
9
- * Profiles are orthogonal to mode governance (Article XI).
10
- * Modes control WHERE; profiles control WHETHER.
11
- *
12
- * Constitution Article XVIII — Execution Profile Governance.
13
- */
14
-
15
- import { existsSync, readFileSync } from 'fs';
16
- import { join } from 'path';
17
-
18
- // ---------------------------------------------------------------------------
19
- // Profile Definitions
20
- // ---------------------------------------------------------------------------
21
-
22
- /**
23
- * Execution profile enum.
24
- * @enum {string}
25
- */
26
- export const Profile = {
27
- /** Read-only. Agents analyze and suggest but perform no writes. */
28
- EXPLORE: 'explore',
29
- /** Default. Agents propose actions and wait for user confirmation. */
30
- GUIDED: 'guided',
31
- /** Full autonomy. Agents execute without confirmation when gates pass. */
32
- AUTONOMOUS: 'autonomous',
33
- };
34
-
35
- /** Minimum gate score required to activate autonomous profile */
36
- export const AUTONOMOUS_GATE_THRESHOLD = 90;
37
-
38
- /**
39
- * Operations that ALWAYS require confirmation regardless of profile.
40
- * Constitution Article XVIII, point 4.
41
- */
42
- export const ALWAYS_CONFIRM_OPERATIONS = [
43
- 'file_delete',
44
- 'database_drop',
45
- 'force_push',
46
- 'deploy_production',
47
- 'deviation_activate',
48
- 'backward_transition',
49
- ];
50
-
51
- // ---------------------------------------------------------------------------
52
- // Profile Management
53
- // ---------------------------------------------------------------------------
54
-
55
- /**
56
- * Get the current execution profile from session state.
57
- *
58
- * @param {string} projectDir
59
- * @returns {string} Current profile (defaults to 'guided')
60
- */
61
- export function getCurrentProfile(projectDir) {
62
- const sessionPath = join(projectDir, '.chati', 'session.yaml');
63
- if (!existsSync(sessionPath)) return Profile.GUIDED;
64
-
65
- const raw = readFileSync(sessionPath, 'utf-8');
66
- const match = raw.match(/^\s*execution_profile:\s*(.+)$/m);
67
- const profile = match ? match[1].trim().replace(/^["']|["']$/g, '') : Profile.GUIDED;
68
-
69
- return Object.values(Profile).includes(profile) ? profile : Profile.GUIDED;
70
- }
71
-
72
- /**
73
- * Check if a write operation is allowed under the current profile.
74
- *
75
- * @param {string} profile - Current execution profile
76
- * @param {string} operation - Operation type (e.g., 'file_write', 'file_delete')
77
- * @returns {{ allowed: boolean, reason: string }}
78
- */
79
- export function isWriteAllowed(profile, operation) {
80
- // Explore profile blocks ALL writes
81
- if (profile === Profile.EXPLORE) {
82
- return { allowed: false, reason: '[Article XVIII] Explore profile: read-only mode. No writes permitted.' };
83
- }
84
-
85
- // Always-confirm operations need user approval regardless of profile
86
- if (ALWAYS_CONFIRM_OPERATIONS.includes(operation)) {
87
- return { allowed: false, reason: `[Article XVIII] Operation "${operation}" always requires user confirmation.` };
88
- }
89
-
90
- // Guided profile: allowed but requires confirmation (caller handles confirmation)
91
- if (profile === Profile.GUIDED) {
92
- return { allowed: true, reason: 'Guided profile: confirmation required.' };
93
- }
94
-
95
- // Autonomous profile: allowed without confirmation
96
- if (profile === Profile.AUTONOMOUS) {
97
- return { allowed: true, reason: 'Autonomous profile: proceeding without confirmation.' };
98
- }
99
-
100
- return { allowed: true, reason: 'Default: allowed.' };
101
- }
102
-
103
- /**
104
- * Validate whether autonomous profile can be activated.
105
- *
106
- * @param {string} projectDir
107
- * @returns {{ canActivate: boolean, reason: string, currentScore: number }}
108
- */
109
- export function canActivateAutonomous(projectDir) {
110
- const sessionPath = join(projectDir, '.chati', 'session.yaml');
111
- if (!existsSync(sessionPath)) {
112
- return { canActivate: false, reason: 'No active session', currentScore: 0 };
113
- }
114
-
115
- const raw = readFileSync(sessionPath, 'utf-8');
116
-
117
- // Look for most recent QA gate score
118
- const scoreMatches = raw.match(/score:\s*([\d.]+)/g);
119
- if (!scoreMatches || scoreMatches.length === 0) {
120
- return { canActivate: false, reason: 'No gate scores found. Run QA gates first.', currentScore: 0 };
121
- }
122
-
123
- const scores = scoreMatches.map((m) => parseFloat(m.replace('score:', '').trim()));
124
- const latestScore = scores[scores.length - 1];
125
-
126
- if (latestScore < AUTONOMOUS_GATE_THRESHOLD) {
127
- return {
128
- canActivate: false,
129
- reason: `Gate score ${latestScore}% < required ${AUTONOMOUS_GATE_THRESHOLD}%`,
130
- currentScore: latestScore,
131
- };
132
- }
133
-
134
- return { canActivate: true, reason: 'Gate scores meet threshold', currentScore: latestScore };
135
- }
136
-
137
- /**
138
- * Check if autonomous profile should be downgraded due to quality drop.
139
- *
140
- * @param {number} currentScore - Current quality score
141
- * @returns {{ shouldDowngrade: boolean, reason: string }}
142
- */
143
- export function shouldDowngradeAutonomous(currentScore) {
144
- if (currentScore < AUTONOMOUS_GATE_THRESHOLD) {
145
- return {
146
- shouldDowngrade: true,
147
- reason: `Quality dropped to ${currentScore}% (< ${AUTONOMOUS_GATE_THRESHOLD}%). Auto-downgrading to guided profile.`,
148
- };
149
- }
150
- return { shouldDowngrade: false, reason: '' };
151
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * @fileoverview File evolution tracker.
3
- *
4
- * Records file modifications by agent for rollback detection,
5
- * conflict prevention in parallel execution, and audit trail.
6
- */
7
-
8
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9
- import { join, dirname } from 'path';
10
-
11
- /**
12
- * @typedef {object} FileEvent
13
- * @property {string} file - Relative file path
14
- * @property {string} agent - Agent that modified the file
15
- * @property {'create'|'modify'|'delete'} action - Type of modification
16
- * @property {string} timestamp - ISO timestamp
17
- * @property {string} [provider] - CLI provider used
18
- */
19
-
20
- const TRACKER_FILE = '.chati/file-evolution.json';
21
-
22
- /**
23
- * Load file evolution history.
24
- *
25
- * @param {string} projectDir
26
- * @returns {FileEvent[]}
27
- */
28
- export function loadHistory(projectDir) {
29
- const trackerPath = join(projectDir, TRACKER_FILE);
30
- if (!existsSync(trackerPath)) return [];
31
- try {
32
- return JSON.parse(readFileSync(trackerPath, 'utf-8'));
33
- } catch {
34
- return [];
35
- }
36
- }
37
-
38
- /**
39
- * Record a file modification event.
40
- *
41
- * @param {string} projectDir
42
- * @param {Omit<FileEvent, 'timestamp'>} event
43
- * @returns {FileEvent}
44
- */
45
- export function recordEvent(projectDir, event) {
46
- const history = loadHistory(projectDir);
47
- const entry = { ...event, timestamp: new Date().toISOString() };
48
- history.push(entry);
49
-
50
- const trackerPath = join(projectDir, TRACKER_FILE);
51
- mkdirSync(dirname(trackerPath), { recursive: true });
52
- writeFileSync(trackerPath, JSON.stringify(history, null, 2));
53
-
54
- return entry;
55
- }
56
-
57
- /**
58
- * Get files modified by a specific agent.
59
- *
60
- * @param {string} projectDir
61
- * @param {string} agent
62
- * @returns {FileEvent[]}
63
- */
64
- export function getAgentFiles(projectDir, agent) {
65
- return loadHistory(projectDir).filter((e) => e.agent === agent);
66
- }
67
-
68
- /**
69
- * Detect potential conflicts between parallel agents.
70
- * Returns files that were modified by multiple agents.
71
- *
72
- * @param {string} projectDir
73
- * @param {string[]} agents - Active parallel agents
74
- * @returns {{ file: string, agents: string[] }[]}
75
- */
76
- export function detectConflicts(projectDir, agents) {
77
- const history = loadHistory(projectDir);
78
- const fileAgentMap = new Map();
79
-
80
- for (const event of history) {
81
- if (agents.includes(event.agent)) {
82
- if (!fileAgentMap.has(event.file)) {
83
- fileAgentMap.set(event.file, new Set());
84
- }
85
- fileAgentMap.get(event.file).add(event.agent);
86
- }
87
- }
88
-
89
- const conflicts = [];
90
- for (const [file, agentSet] of fileAgentMap) {
91
- if (agentSet.size > 1) {
92
- conflicts.push({ file, agents: [...agentSet] });
93
- }
94
- }
95
-
96
- return conflicts;
97
- }
98
-
99
- /**
100
- * Prune history older than specified days.
101
- *
102
- * @param {string} projectDir
103
- * @param {number} [days=30]
104
- * @returns {number} Number of entries pruned
105
- */
106
- export function pruneHistory(projectDir, days = 30) {
107
- const history = loadHistory(projectDir);
108
- const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
109
- const filtered = history.filter((e) => new Date(e.timestamp).getTime() > cutoff);
110
- const pruned = history.length - filtered.length;
111
-
112
- const trackerPath = join(projectDir, TRACKER_FILE);
113
- mkdirSync(dirname(trackerPath), { recursive: true });
114
- writeFileSync(trackerPath, JSON.stringify(filtered, null, 2));
115
-
116
- return pruned;
117
- }
@@ -1,253 +0,0 @@
1
- /**
2
- * @fileoverview Gotchas auto-capture engine.
3
- *
4
- * Monitors agent execution for recurring error patterns.
5
- * When an error pattern appears 3+ times, it is automatically
6
- * captured as a gotcha and injected before related tasks.
7
- *
8
- * Constitution Article XIII — Memory Governance.
9
- */
10
-
11
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
12
- import { join, dirname } from 'path';
13
-
14
- // ---------------------------------------------------------------------------
15
- // Constants
16
- // ---------------------------------------------------------------------------
17
-
18
- /** Minimum occurrences before auto-capture triggers */
19
- const AUTO_CAPTURE_THRESHOLD = 3;
20
-
21
- /** Time window for counting occurrences (24 hours) */
22
- const CAPTURE_WINDOW_MS = 24 * 60 * 60 * 1000;
23
-
24
- /** Maximum age before a gotcha is archived (90 days) */
25
- const ARCHIVE_AGE_MS = 90 * 24 * 60 * 60 * 1000;
26
-
27
- /**
28
- * Error categories for classification.
29
- * @enum {string}
30
- */
31
- const Category = {
32
- BUILD: 'build',
33
- TEST: 'test',
34
- LINT: 'lint',
35
- RUNTIME: 'runtime',
36
- INTEGRATION: 'integration',
37
- SECURITY: 'security',
38
- };
39
-
40
- /**
41
- * Severity levels.
42
- * @enum {string}
43
- */
44
- const Severity = {
45
- INFO: 'info',
46
- WARNING: 'warning',
47
- CRITICAL: 'critical',
48
- };
49
-
50
- // ---------------------------------------------------------------------------
51
- // Error Pattern Matching
52
- // ---------------------------------------------------------------------------
53
-
54
- /**
55
- * Known error pattern signatures for classification.
56
- * Each pattern has a regex, category, and severity.
57
- */
58
- const ERROR_PATTERNS = [
59
- { regex: /Cannot find module/i, category: Category.BUILD, severity: Severity.WARNING },
60
- { regex: /SyntaxError/i, category: Category.BUILD, severity: Severity.CRITICAL },
61
- { regex: /TypeError/i, category: Category.RUNTIME, severity: Severity.WARNING },
62
- { regex: /ENOENT/i, category: Category.BUILD, severity: Severity.WARNING },
63
- { regex: /EACCES/i, category: Category.SECURITY, severity: Severity.CRITICAL },
64
- { regex: /test.*fail/i, category: Category.TEST, severity: Severity.WARNING },
65
- { regex: /lint.*error/i, category: Category.LINT, severity: Severity.INFO },
66
- { regex: /CORS/i, category: Category.INTEGRATION, severity: Severity.WARNING },
67
- { regex: /401|403|unauthorized/i, category: Category.SECURITY, severity: Severity.CRITICAL },
68
- { regex: /timeout|ETIMEDOUT/i, category: Category.INTEGRATION, severity: Severity.WARNING },
69
- { regex: /out of memory|heap/i, category: Category.RUNTIME, severity: Severity.CRITICAL },
70
- { regex: /deprecated/i, category: Category.BUILD, severity: Severity.INFO },
71
- ];
72
-
73
- // ---------------------------------------------------------------------------
74
- // Auto-Capture Engine
75
- // ---------------------------------------------------------------------------
76
-
77
- /**
78
- * Normalize an error message into a stable key for deduplication.
79
- *
80
- * @param {string} message - Raw error message
81
- * @returns {string} Normalized key
82
- */
83
- export function normalizeErrorKey(message) {
84
- return message
85
- .replace(/['"][^'"]*['"]/g, '""') // Replace string literals
86
- .replace(/\d+/g, 'N') // Replace numbers
87
- .replace(/\/[^\s]+/g, '/PATH') // Replace file paths
88
- .replace(/\s+/g, ' ') // Normalize whitespace
89
- .trim()
90
- .slice(0, 200); // Limit length
91
- }
92
-
93
- /**
94
- * Classify an error message into category and severity.
95
- *
96
- * @param {string} message - Error message
97
- * @returns {{ category: string, severity: string }}
98
- */
99
- export function classifyError(message) {
100
- for (const pattern of ERROR_PATTERNS) {
101
- if (pattern.regex.test(message)) {
102
- return { category: pattern.category, severity: pattern.severity };
103
- }
104
- }
105
- return { category: Category.RUNTIME, severity: Severity.INFO };
106
- }
107
-
108
- /**
109
- * Load the error tracker state from disk.
110
- *
111
- * @param {string} projectDir - Project root
112
- * @returns {{ errors: Record<string, { count: number, firstSeen: string, lastSeen: string, message: string, category: string, severity: string }>, gotchas: string[] }}
113
- */
114
- export function loadTrackerState(projectDir) {
115
- const trackerPath = join(projectDir, '.chati', 'error-tracker.json');
116
- if (!existsSync(trackerPath)) {
117
- return { errors: {}, gotchas: [] };
118
- }
119
- try {
120
- return JSON.parse(readFileSync(trackerPath, 'utf-8'));
121
- } catch {
122
- return { errors: {}, gotchas: [] };
123
- }
124
- }
125
-
126
- /**
127
- * Save the error tracker state to disk.
128
- *
129
- * @param {string} projectDir - Project root
130
- * @param {object} state - Tracker state
131
- */
132
- export function saveTrackerState(projectDir, state) {
133
- const trackerPath = join(projectDir, '.chati', 'error-tracker.json');
134
- mkdirSync(dirname(trackerPath), { recursive: true });
135
- writeFileSync(trackerPath, JSON.stringify(state, null, 2));
136
- }
137
-
138
- /**
139
- * Track an error occurrence. If threshold is met, auto-create a gotcha.
140
- *
141
- * @param {string} projectDir - Project root
142
- * @param {string} errorMessage - Raw error message
143
- * @param {string} [agent] - Agent that encountered the error
144
- * @returns {{ captured: boolean, gotcha: object|null }}
145
- */
146
- export function trackError(projectDir, errorMessage, agent) {
147
- const state = loadTrackerState(projectDir);
148
- const key = normalizeErrorKey(errorMessage);
149
- const { category, severity } = classifyError(errorMessage);
150
- const now = new Date().toISOString();
151
-
152
- // Initialize or update error entry
153
- if (!state.errors[key]) {
154
- state.errors[key] = {
155
- count: 0,
156
- firstSeen: now,
157
- lastSeen: now,
158
- message: errorMessage.slice(0, 500),
159
- category,
160
- severity,
161
- };
162
- }
163
-
164
- const entry = state.errors[key];
165
-
166
- // Only count within the capture window
167
- const lastSeen = new Date(entry.lastSeen).getTime();
168
- if (Date.now() - lastSeen > CAPTURE_WINDOW_MS) {
169
- entry.count = 0; // Reset if outside window
170
- }
171
-
172
- entry.count += 1;
173
- entry.lastSeen = now;
174
-
175
- // Check if threshold is met and gotcha not already created
176
- let gotcha = null;
177
- let captured = false;
178
-
179
- if (entry.count >= AUTO_CAPTURE_THRESHOLD && !state.gotchas.includes(key)) {
180
- gotcha = {
181
- id: `G-AUTO-${Date.now()}`,
182
- pattern: key,
183
- description: entry.message,
184
- category: entry.category,
185
- severity: entry.severity,
186
- discovered_by: agent || 'auto-capture',
187
- discovered_at: now,
188
- occurrence_count: entry.count,
189
- auto_captured: true,
190
- };
191
-
192
- state.gotchas.push(key);
193
- captured = true;
194
-
195
- // Append to gotchas runtime file
196
- const gotchasPath = join(projectDir, '.chati', 'gotchas.json');
197
- let gotchasList = [];
198
- if (existsSync(gotchasPath)) {
199
- try {
200
- gotchasList = JSON.parse(readFileSync(gotchasPath, 'utf-8'));
201
- } catch { /* ignore */ }
202
- }
203
- gotchasList.push(gotcha);
204
- writeFileSync(gotchasPath, JSON.stringify(gotchasList, null, 2));
205
- }
206
-
207
- saveTrackerState(projectDir, state);
208
- return { captured, gotcha };
209
- }
210
-
211
- /**
212
- * Prune old errors outside the capture window and archive old gotchas.
213
- *
214
- * @param {string} projectDir - Project root
215
- * @returns {{ pruned: number, archived: number }}
216
- */
217
- export function pruneTracker(projectDir) {
218
- const state = loadTrackerState(projectDir);
219
- const now = Date.now();
220
- let pruned = 0;
221
- let archived = 0;
222
-
223
- // Prune old error entries
224
- for (const [key, entry] of Object.entries(state.errors)) {
225
- const lastSeen = new Date(entry.lastSeen).getTime();
226
- if (now - lastSeen > CAPTURE_WINDOW_MS) {
227
- delete state.errors[key];
228
- pruned++;
229
- }
230
- }
231
-
232
- // Archive old gotchas
233
- const gotchasPath = join(projectDir, '.chati', 'gotchas.json');
234
- if (existsSync(gotchasPath)) {
235
- try {
236
- const gotchasList = JSON.parse(readFileSync(gotchasPath, 'utf-8'));
237
- const active = gotchasList.filter((g) => {
238
- const age = now - new Date(g.discovered_at).getTime();
239
- if (age > ARCHIVE_AGE_MS) {
240
- archived++;
241
- return false;
242
- }
243
- return true;
244
- });
245
- writeFileSync(gotchasPath, JSON.stringify(active, null, 2));
246
- } catch { /* ignore */ }
247
- }
248
-
249
- saveTrackerState(projectDir, state);
250
- return { pruned, archived };
251
- }
252
-
253
- export { AUTO_CAPTURE_THRESHOLD, CAPTURE_WINDOW_MS, ARCHIVE_AGE_MS, Category, Severity, ERROR_PATTERNS };