chati-dev 3.2.4 → 3.3.0
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/LICENSE +96 -0
- package/README.md +2 -2
- package/bin/chati.js +46 -0
- package/framework/agents/build/dev.md +122 -1
- package/framework/agents/deploy/devops.md +128 -3
- package/framework/agents/discover/brief.md +77 -15
- package/framework/agents/discover/brownfield-wu.md +2 -2
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +3 -3
- package/framework/agents/plan/phases.md +127 -2
- package/framework/agents/plan/tasks.md +127 -2
- package/framework/agents/plan/ux.md +269 -22
- package/framework/agents/quality/qa-implementation.md +172 -8
- package/framework/agents/quality/qa-planning.md +147 -2
- package/framework/config.yaml +9 -5
- package/framework/constitution.md +7 -1
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +1 -1
- package/framework/hooks/constitution-guard.js +18 -2
- package/framework/hooks/mode-governance.js +3 -3
- package/framework/hooks/read-protection.js +10 -2
- package/framework/i18n/en.yaml +6 -0
- package/framework/i18n/es.yaml +6 -0
- package/framework/i18n/fr.yaml +6 -0
- package/framework/i18n/pt.yaml +6 -0
- package/framework/orchestrator/chati.md +102 -6
- package/framework/schemas/task.schema.json +1 -1
- package/framework/tasks/architect-dep-audit.md +128 -0
- package/framework/tasks/architect-stack-selection.md +28 -0
- package/framework/workflows/brownfield-fullstack.yaml +2 -2
- package/framework/workflows/brownfield-service.yaml +2 -2
- package/framework/workflows/brownfield-ui.yaml +2 -2
- package/framework/workflows/greenfield-fullstack.yaml +6 -2
- package/framework/workflows/quick-flow.yaml +7 -5
- package/framework/workflows/standard-flow.yaml +171 -0
- package/package.json +5 -3
- package/src/api/index.js +129 -0
- package/src/autonomy/build-loop.js +93 -6
- package/src/autonomy/build-state.js +20 -2
- package/src/autonomy/cause-analyzer.js +177 -0
- package/src/autonomy/escalation.js +214 -0
- package/src/autonomy/safety-net.js +23 -5
- package/src/autonomy/worktree-manager.js +245 -0
- package/src/config/agent-customizer.js +227 -0
- package/src/config/ide-configs.js +57 -27
- package/src/decision/analyzer.js +148 -0
- package/src/decision/registry-healer.js +38 -21
- package/src/extensions/loader.js +151 -0
- package/src/extensions/registry.js +134 -0
- package/src/gates/circuit-breaker.js +32 -0
- package/src/gates/g3-implementation.js +30 -4
- package/src/gates/g4-qa-implementation.js +34 -5
- package/src/gates/gate-base.js +9 -0
- package/src/health/auto-fix.js +216 -0
- package/src/installer/core.js +24 -11
- package/src/installer/provider-overlay.js +82 -0
- package/src/installer/templates.js +22 -10
- package/src/installer/transaction.js +3 -2
- package/src/installer/validator.js +74 -0
- package/src/intelligence/context-status.js +9 -5
- package/src/intelligence/document-sharder.js +221 -0
- package/src/intelligence/elicitation.js +265 -0
- package/src/intelligence/timeline.js +5 -0
- package/src/memory/gotchas.js +78 -2
- package/src/merger/semantic-merger.js +292 -0
- package/src/orchestrator/agent-selector.js +20 -0
- package/src/orchestrator/handoff-engine.js +77 -0
- package/src/orchestrator/index.js +0 -8
- package/src/orchestrator/intent-classifier.js +182 -0
- package/src/orchestrator/pipeline-manager.js +125 -1
- package/src/orchestrator/session-manager.js +164 -2
- package/src/quality/metrics-collector.js +283 -0
- package/src/quality/test-runner.js +368 -0
- package/src/telemetry/collector.js +83 -0
- package/src/telemetry/config.js +119 -0
- package/src/telemetry/index.js +11 -0
- package/src/telemetry/schema.js +104 -0
- package/src/telemetry/sender.js +60 -0
- package/src/terminal/cli-registry.js +7 -1
- package/src/terminal/cost-tracker.js +197 -0
- package/src/terminal/handoff-parser.js +61 -4
- package/src/terminal/prompt-builder.js +56 -18
- package/src/terminal/rate-limiter.js +172 -0
- package/src/terminal/run-agent.js +39 -0
- package/src/terminal/run-parallel.js +22 -1
- package/src/terminal/spawner.js +181 -3
- package/src/upgrade/migrator.js +2 -2
- package/src/utils/event-bus.js +126 -0
- package/src/utils/file-lock.js +291 -0
- package/src/utils/schema-validator.js +226 -0
- package/src/wizard/i18n.js +11 -0
- package/src/wizard/index.js +42 -20
- package/src/wizard/questions.js +200 -39
- package/src/autonomy/execution-profile.js +0 -151
- package/src/intelligence/file-tracker.js +0 -117
- package/src/memory/gotchas-auto-capture.js +0 -253
- package/src/orchestrator/pipeline-state.js +0 -223
- 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 };
|