chati-dev 2.1.2 → 3.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 +95 -24
- package/bin/chati.js +1 -1
- package/framework/agents/build/dev.md +1 -0
- package/framework/agents/deploy/devops.md +1 -0
- package/framework/agents/discover/brief.md +1 -0
- package/framework/agents/discover/brownfield-wu.md +1 -0
- package/framework/agents/discover/greenfield-wu.md +1 -0
- package/framework/agents/plan/architect.md +1 -0
- package/framework/agents/plan/detail.md +1 -0
- package/framework/agents/plan/phases.md +3 -2
- package/framework/agents/plan/tasks.md +1 -0
- package/framework/agents/plan/ux.md +1 -0
- package/framework/agents/quality/qa-implementation.md +1 -0
- package/framework/agents/quality/qa-planning.md +1 -0
- package/framework/config.yaml +21 -2
- package/framework/constitution.md +67 -2
- package/framework/context/governance.md +16 -3
- package/framework/context/root.md +3 -3
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/brownfield-wu.yaml +4 -0
- package/framework/domains/agents/detail.yaml +1 -1
- package/framework/domains/agents/dev.yaml +4 -0
- package/framework/domains/agents/orchestrator.yaml +8 -0
- package/framework/domains/agents/phases.yaml +2 -2
- package/framework/domains/agents/tasks.yaml +2 -2
- package/framework/domains/constitution.yaml +30 -2
- package/framework/domains/global.yaml +20 -0
- package/framework/hooks/model-governance.js +17 -15
- package/framework/hooks/read-protection.js +1 -0
- package/framework/i18n/en.yaml +11 -4
- package/framework/i18n/es.yaml +11 -4
- package/framework/i18n/fr.yaml +11 -4
- package/framework/i18n/pt.yaml +11 -4
- package/framework/intelligence/context-engine.md +31 -2
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/intelligence/memory-layer.md +17 -0
- package/framework/orchestrator/chati.md +94 -14
- package/framework/patterns/elicitation-library.yaml +2 -2
- package/framework/schemas/config.schema.json +44 -0
- package/framework/schemas/session.schema.json +27 -0
- package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
- package/framework/tasks/brownfield-wu-migration-plan.md +4 -4
- package/framework/tasks/brownfield-wu-risk-assess.md +2 -2
- package/framework/tasks/detail-expand-prd.md +2 -2
- package/framework/tasks/greenfield-wu-report.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +2 -2
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/qa-impl-performance-test.md +2 -2
- package/framework/tasks/qa-impl-sast-scan.md +6 -6
- package/framework/tasks/qa-planning-consolidate.md +1 -1
- package/framework/tasks/qa-planning-coverage-plan.md +9 -9
- package/framework/tasks/qa-planning-gate-define.md +6 -6
- package/framework/tasks/qa-planning-risk-matrix.md +1 -1
- package/framework/tasks/tasks-consolidate.md +5 -5
- package/package.json +5 -1
- package/scripts/changelog-generator.js +2 -2
- package/scripts/ide-sync.js +9 -4
- package/scripts/pr-review.js +5 -5
- package/scripts/semantic-lint.js +3 -3
- package/src/autonomy/build-loop.js +194 -0
- package/src/autonomy/build-state.js +269 -0
- package/src/autonomy/execution-profile.js +151 -0
- package/src/config/context-file-generator.js +203 -0
- package/src/config/ide-configs.js +6 -6
- package/src/gates/g2-qa-planning.js +4 -2
- package/src/gates/g4-qa-implementation.js +5 -2
- package/src/gates/gate-base.js +33 -1
- package/src/health/engine.js +245 -0
- package/src/installer/core.js +54 -16
- package/src/installer/templates.js +131 -2
- package/src/installer/validator.js +5 -3
- package/src/intelligence/file-tracker.js +117 -0
- package/src/intelligence/registry-manager.js +2 -2
- package/src/intelligence/timeline.js +144 -0
- package/src/memory/gotchas-auto-capture.js +253 -0
- package/src/preview/detector.js +1 -1
- package/src/terminal/adapters/claude-adapter.js +33 -0
- package/src/terminal/adapters/codex-adapter.js +31 -0
- package/src/terminal/adapters/copilot-adapter.js +28 -0
- package/src/terminal/adapters/gemini-adapter.js +28 -0
- package/src/terminal/adapters/index.js +8 -0
- package/src/terminal/cli-registry.js +190 -0
- package/src/terminal/prompt-builder.js +9 -20
- package/src/terminal/run-agent.js +2 -1
- package/src/terminal/spawner.js +44 -14
- package/src/terminal/wave-analyzer.js +143 -0
- package/src/utils/config-parser.js +97 -0
- package/src/wizard/feedback.js +1 -1
- package/src/wizard/i18n.js +11 -4
- package/src/wizard/index.js +22 -10
- package/src/wizard/questions.js +50 -2
- package/framework/manifest.json +0 -5
- package/framework/manifest.sig +0 -1
- /package/assets/{logo - co/314/201pia.png" → logo - c/303/263pia.png"} +0 -0
- /package/assets/{logo - co/314/201pia.svg" → logo - c/303/263pia.svg"} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
}
|
|
@@ -131,8 +131,8 @@ export function runHealthCheck(targetDir) {
|
|
|
131
131
|
if (existsSync(constitutionPath)) {
|
|
132
132
|
const content = readFileSync(constitutionPath, 'utf-8');
|
|
133
133
|
const articleCount = (content.match(/^## Article/gm) || []).length;
|
|
134
|
-
checks.constitution.pass = articleCount >=
|
|
135
|
-
checks.constitution.details = `${articleCount}/
|
|
134
|
+
checks.constitution.pass = articleCount >= 19;
|
|
135
|
+
checks.constitution.details = `${articleCount}/19 articles`;
|
|
136
136
|
} else {
|
|
137
137
|
checks.constitution.details = 'Not found';
|
|
138
138
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Session timeline manager.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a chronological record of all significant events
|
|
5
|
+
* during a session: agent activations, mode transitions, gate
|
|
6
|
+
* results, handoffs, and deviations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} TimelineEvent
|
|
14
|
+
* @property {string} id - Unique event ID
|
|
15
|
+
* @property {string} type - Event type
|
|
16
|
+
* @property {string} agent - Agent involved (or 'orchestrator')
|
|
17
|
+
* @property {object} data - Event-specific data
|
|
18
|
+
* @property {string} timestamp - ISO timestamp
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Event type constants.
|
|
23
|
+
*/
|
|
24
|
+
export const EventType = {
|
|
25
|
+
AGENT_ACTIVATED: 'agent_activated',
|
|
26
|
+
AGENT_COMPLETED: 'agent_completed',
|
|
27
|
+
MODE_TRANSITION: 'mode_transition',
|
|
28
|
+
PROFILE_TRANSITION: 'profile_transition',
|
|
29
|
+
GATE_EVALUATED: 'gate_evaluated',
|
|
30
|
+
HANDOFF_CREATED: 'handoff_created',
|
|
31
|
+
DEVIATION_DETECTED: 'deviation_detected',
|
|
32
|
+
ERROR_OCCURRED: 'error_occurred',
|
|
33
|
+
PROVIDER_SELECTED: 'provider_selected',
|
|
34
|
+
SESSION_STARTED: 'session_started',
|
|
35
|
+
SESSION_ENDED: 'session_ended',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const TIMELINE_FILE = '.chati/timeline.json';
|
|
39
|
+
let _counter = 0;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load the timeline from disk.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} projectDir
|
|
45
|
+
* @returns {TimelineEvent[]}
|
|
46
|
+
*/
|
|
47
|
+
export function loadTimeline(projectDir) {
|
|
48
|
+
const timelinePath = join(projectDir, TIMELINE_FILE);
|
|
49
|
+
if (!existsSync(timelinePath)) return [];
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(timelinePath, 'utf-8'));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save the timeline to disk.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} projectDir
|
|
61
|
+
* @param {TimelineEvent[]} timeline
|
|
62
|
+
*/
|
|
63
|
+
function saveTimeline(projectDir, timeline) {
|
|
64
|
+
const timelinePath = join(projectDir, TIMELINE_FILE);
|
|
65
|
+
mkdirSync(dirname(timelinePath), { recursive: true });
|
|
66
|
+
writeFileSync(timelinePath, JSON.stringify(timeline, null, 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record a new event on the timeline.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} projectDir
|
|
73
|
+
* @param {string} type - Event type from EventType
|
|
74
|
+
* @param {string} agent - Agent name
|
|
75
|
+
* @param {object} [data={}] - Event-specific data
|
|
76
|
+
* @returns {TimelineEvent}
|
|
77
|
+
*/
|
|
78
|
+
export function recordEvent(projectDir, type, agent, data = {}) {
|
|
79
|
+
const timeline = loadTimeline(projectDir);
|
|
80
|
+
_counter++;
|
|
81
|
+
|
|
82
|
+
const event = {
|
|
83
|
+
id: `evt-${Date.now()}-${_counter}`,
|
|
84
|
+
type,
|
|
85
|
+
agent,
|
|
86
|
+
data,
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
timeline.push(event);
|
|
91
|
+
saveTimeline(projectDir, timeline);
|
|
92
|
+
return event;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get events filtered by type.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} projectDir
|
|
99
|
+
* @param {string} type
|
|
100
|
+
* @returns {TimelineEvent[]}
|
|
101
|
+
*/
|
|
102
|
+
export function getEventsByType(projectDir, type) {
|
|
103
|
+
return loadTimeline(projectDir).filter((e) => e.type === type);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get events filtered by agent.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} projectDir
|
|
110
|
+
* @param {string} agent
|
|
111
|
+
* @returns {TimelineEvent[]}
|
|
112
|
+
*/
|
|
113
|
+
export function getEventsByAgent(projectDir, agent) {
|
|
114
|
+
return loadTimeline(projectDir).filter((e) => e.agent === agent);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the most recent event of a given type.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} projectDir
|
|
121
|
+
* @param {string} type
|
|
122
|
+
* @returns {TimelineEvent|null}
|
|
123
|
+
*/
|
|
124
|
+
export function getLatestEvent(projectDir, type) {
|
|
125
|
+
const events = getEventsByType(projectDir, type);
|
|
126
|
+
return events.length > 0 ? events[events.length - 1] : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Clear the timeline (used for session reset).
|
|
131
|
+
*
|
|
132
|
+
* @param {string} projectDir
|
|
133
|
+
*/
|
|
134
|
+
export function clearTimeline(projectDir) {
|
|
135
|
+
saveTimeline(projectDir, []);
|
|
136
|
+
_counter = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reset counter (for tests).
|
|
141
|
+
*/
|
|
142
|
+
export function _resetCounter() {
|
|
143
|
+
_counter = 0;
|
|
144
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
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 };
|
package/src/preview/detector.js
CHANGED
|
@@ -191,7 +191,7 @@ export function detectProjectKind(projectDir) {
|
|
|
191
191
|
* @returns {{ command: string, args: string[], description: string, framework: string, defaultPort: number }}
|
|
192
192
|
*/
|
|
193
193
|
function resolveFrameworkCommand(projectDir, detector) {
|
|
194
|
-
const { framework, defaultPort
|
|
194
|
+
const { framework, defaultPort } = detector;
|
|
195
195
|
|
|
196
196
|
// Django has its own command
|
|
197
197
|
if (framework === 'django') {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Claude Code CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* Translates chati.dev spawning config into Claude Code CLI
|
|
5
|
+
* command, arguments, and environment variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('../cli-registry.js').ProviderConfig} ProviderConfig
|
|
10
|
+
* @typedef {import('../spawner.js').SpawnConfig} SpawnConfig
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build Claude Code CLI command and arguments.
|
|
15
|
+
*
|
|
16
|
+
* @param {SpawnConfig} config - Spawn configuration
|
|
17
|
+
* @param {ProviderConfig} provider - Provider definition from registry
|
|
18
|
+
* @returns {{ command: string, args: string[], stdinPrompt: string|null }}
|
|
19
|
+
*/
|
|
20
|
+
export function buildCommand(config, provider) {
|
|
21
|
+
const args = [...provider.baseArgs];
|
|
22
|
+
|
|
23
|
+
if (config.model) {
|
|
24
|
+
const resolvedModel = provider.modelMap[config.model] || config.model;
|
|
25
|
+
args.push(provider.modelFlag, resolvedModel);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
command: provider.command,
|
|
30
|
+
args,
|
|
31
|
+
stdinPrompt: config.prompt || null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview OpenAI Codex CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* Translates chati.dev spawning config into Codex CLI
|
|
5
|
+
* command, arguments, and environment variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build Codex CLI command and arguments.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('../spawner.js').SpawnConfig} config
|
|
12
|
+
* @param {import('../cli-registry.js').ProviderConfig} provider
|
|
13
|
+
* @returns {{ command: string, args: string[], stdinPrompt: string|null }}
|
|
14
|
+
*/
|
|
15
|
+
export function buildCommand(config, provider) {
|
|
16
|
+
const args = [...provider.baseArgs];
|
|
17
|
+
|
|
18
|
+
if (config.model) {
|
|
19
|
+
const resolvedModel = provider.modelMap[config.model] || config.model;
|
|
20
|
+
args.push(provider.modelFlag, resolvedModel);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Codex exec reads prompt from stdin when `-` is passed
|
|
24
|
+
args.push('-');
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
command: provider.command,
|
|
28
|
+
args,
|
|
29
|
+
stdinPrompt: config.prompt || null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview GitHub Copilot CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* Translates chati.dev spawning config into Copilot CLI
|
|
5
|
+
* command, arguments, and environment variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build Copilot CLI command and arguments.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('../spawner.js').SpawnConfig} config
|
|
12
|
+
* @param {import('../cli-registry.js').ProviderConfig} provider
|
|
13
|
+
* @returns {{ command: string, args: string[], stdinPrompt: string|null }}
|
|
14
|
+
*/
|
|
15
|
+
export function buildCommand(config, provider) {
|
|
16
|
+
const args = [...provider.baseArgs];
|
|
17
|
+
|
|
18
|
+
if (config.model) {
|
|
19
|
+
const resolvedModel = provider.modelMap[config.model] || config.model;
|
|
20
|
+
args.push(provider.modelFlag, resolvedModel);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
command: provider.command,
|
|
25
|
+
args,
|
|
26
|
+
stdinPrompt: config.prompt || null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Gemini CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* Translates chati.dev spawning config into Gemini CLI
|
|
5
|
+
* command, arguments, and environment variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build Gemini CLI command and arguments.
|
|
10
|
+
*
|
|
11
|
+
* @param {import('../spawner.js').SpawnConfig} config
|
|
12
|
+
* @param {import('../cli-registry.js').ProviderConfig} provider
|
|
13
|
+
* @returns {{ command: string, args: string[], stdinPrompt: string|null }}
|
|
14
|
+
*/
|
|
15
|
+
export function buildCommand(config, provider) {
|
|
16
|
+
const args = [...provider.baseArgs];
|
|
17
|
+
|
|
18
|
+
if (config.model) {
|
|
19
|
+
const resolvedModel = provider.modelMap[config.model] || config.model;
|
|
20
|
+
args.push(provider.modelFlag, resolvedModel);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
command: provider.command,
|
|
25
|
+
args,
|
|
26
|
+
stdinPrompt: config.prompt || null,
|
|
27
|
+
};
|
|
28
|
+
}
|