chati-dev 2.1.2 → 3.0.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.
Files changed (50) hide show
  1. package/framework/agents/build/dev.md +1 -0
  2. package/framework/agents/deploy/devops.md +1 -0
  3. package/framework/agents/discover/brief.md +1 -0
  4. package/framework/agents/discover/brownfield-wu.md +1 -0
  5. package/framework/agents/discover/greenfield-wu.md +1 -0
  6. package/framework/agents/plan/architect.md +1 -0
  7. package/framework/agents/plan/detail.md +1 -0
  8. package/framework/agents/plan/phases.md +1 -0
  9. package/framework/agents/plan/tasks.md +1 -0
  10. package/framework/agents/plan/ux.md +1 -0
  11. package/framework/agents/quality/qa-implementation.md +1 -0
  12. package/framework/agents/quality/qa-planning.md +1 -0
  13. package/framework/config.yaml +21 -2
  14. package/framework/constitution.md +66 -1
  15. package/framework/domains/agents/brownfield-wu.yaml +4 -0
  16. package/framework/domains/agents/dev.yaml +4 -0
  17. package/framework/domains/agents/orchestrator.yaml +8 -0
  18. package/framework/domains/constitution.yaml +28 -0
  19. package/framework/domains/global.yaml +20 -0
  20. package/framework/hooks/model-governance.js +17 -15
  21. package/framework/intelligence/context-engine.md +29 -0
  22. package/framework/intelligence/memory-layer.md +17 -0
  23. package/framework/orchestrator/chati.md +94 -14
  24. package/framework/schemas/config.schema.json +44 -0
  25. package/framework/schemas/session.schema.json +27 -0
  26. package/package.json +5 -1
  27. package/src/autonomy/build-loop.js +194 -0
  28. package/src/autonomy/build-state.js +269 -0
  29. package/src/autonomy/execution-profile.js +151 -0
  30. package/src/config/context-file-generator.js +209 -0
  31. package/src/gates/g2-qa-planning.js +4 -2
  32. package/src/gates/g4-qa-implementation.js +5 -2
  33. package/src/gates/gate-base.js +33 -1
  34. package/src/health/engine.js +250 -0
  35. package/src/intelligence/file-tracker.js +117 -0
  36. package/src/intelligence/timeline.js +144 -0
  37. package/src/memory/gotchas-auto-capture.js +253 -0
  38. package/src/terminal/adapters/claude-adapter.js +43 -0
  39. package/src/terminal/adapters/codex-adapter.js +41 -0
  40. package/src/terminal/adapters/copilot-adapter.js +38 -0
  41. package/src/terminal/adapters/gemini-adapter.js +42 -0
  42. package/src/terminal/adapters/index.js +8 -0
  43. package/src/terminal/cli-registry.js +218 -0
  44. package/src/terminal/prompt-builder.js +18 -15
  45. package/src/terminal/spawner.js +19 -9
  46. package/src/terminal/wave-analyzer.js +143 -0
  47. package/framework/manifest.json +0 -5
  48. package/framework/manifest.sig +0 -1
  49. /package/assets/{logo - co/314/201pia.png" → logo - c/303/263pia.png"} +0 -0
  50. /package/assets/{logo - co/314/201pia.svg" → logo - c/303/263pia.svg"} +0 -0
@@ -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 };
@@ -0,0 +1,43 @@
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 = ['--print', '--dangerously-skip-permissions'];
22
+
23
+ if (config.model) {
24
+ const resolvedModel = provider.modelMap[config.model] || config.model;
25
+ args.push('--model', resolvedModel);
26
+ }
27
+
28
+ return {
29
+ command: 'claude',
30
+ args,
31
+ stdinPrompt: config.prompt || null,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Build environment variables specific to Claude Code.
37
+ *
38
+ * @param {SpawnConfig} config
39
+ * @returns {Record<string, string>}
40
+ */
41
+ export function buildEnv(config) {
42
+ return {};
43
+ }
@@ -0,0 +1,41 @@
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 = ['exec'];
17
+
18
+ if (config.model) {
19
+ const resolvedModel = provider.modelMap[config.model] || config.model;
20
+ args.push('-m', resolvedModel);
21
+ }
22
+
23
+ // Codex exec reads prompt from stdin when `-` is passed
24
+ args.push('-');
25
+
26
+ return {
27
+ command: 'codex',
28
+ args,
29
+ stdinPrompt: config.prompt || null,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Build environment variables specific to Codex CLI.
35
+ *
36
+ * @param {import('../spawner.js').SpawnConfig} config
37
+ * @returns {Record<string, string>}
38
+ */
39
+ export function buildEnv(config) {
40
+ return {};
41
+ }
@@ -0,0 +1,38 @@
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 = ['-p'];
17
+
18
+ if (config.model) {
19
+ const resolvedModel = provider.modelMap[config.model] || config.model;
20
+ args.push('--model', resolvedModel);
21
+ }
22
+
23
+ return {
24
+ command: 'copilot',
25
+ args,
26
+ stdinPrompt: config.prompt || null,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Build environment variables specific to Copilot CLI.
32
+ *
33
+ * @param {import('../spawner.js').SpawnConfig} config
34
+ * @returns {Record<string, string>}
35
+ */
36
+ export function buildEnv(config) {
37
+ return {};
38
+ }
@@ -0,0 +1,42 @@
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 = [];
17
+
18
+ if (config.model) {
19
+ const resolvedModel = provider.modelMap[config.model] || config.model;
20
+ args.push('--model', resolvedModel);
21
+ }
22
+
23
+ // Gemini CLI uses --prompt for non-interactive mode
24
+ // When stdin is piped, Gemini reads from stdin automatically
25
+ args.push('--prompt');
26
+
27
+ return {
28
+ command: 'gemini',
29
+ args,
30
+ stdinPrompt: config.prompt || null,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Build environment variables specific to Gemini CLI.
36
+ *
37
+ * @param {import('../spawner.js').SpawnConfig} config
38
+ * @returns {Record<string, string>}
39
+ */
40
+ export function buildEnv(config) {
41
+ return {};
42
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Barrel export for CLI adapters.
3
+ */
4
+
5
+ export * as claude from './claude-adapter.js';
6
+ export * as gemini from './gemini-adapter.js';
7
+ export * as codex from './codex-adapter.js';
8
+ export * as copilot from './copilot-adapter.js';