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,209 @@
1
+ /**
2
+ * @fileoverview Context file generator for multi-CLI support.
3
+ *
4
+ * When alternative CLI providers are enabled, generates provider-specific
5
+ * context files (GEMINI.md, AGENTS.md) derived from CLAUDE.md content.
6
+ * Constitution Article XIX — context file generation is automatic.
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Replacement Maps
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Text replacements for Gemini CLI context file.
18
+ * Transforms Claude Code-specific references into Gemini CLI equivalents.
19
+ */
20
+ const GEMINI_REPLACEMENTS = [
21
+ ['Claude Code', 'Gemini CLI'],
22
+ ['claude code', 'Gemini CLI'],
23
+ ['CLAUDE.md', 'GEMINI.md'],
24
+ ['Claude.md', 'GEMINI.md'],
25
+ ['.claude/commands/', '.gemini/agents/'],
26
+ ['.claude/rules/', '.gemini/rules/'],
27
+ ['.claude/mcp.json', '.gemini/settings.json'],
28
+ ['claude --print', 'gemini --prompt'],
29
+ ['claude -p', 'gemini --prompt'],
30
+ ['CLAUDE.local.md', 'GEMINI.local.md'],
31
+ ];
32
+
33
+ /**
34
+ * Sections to strip from Codex CLI context file.
35
+ * Codex does not support hooks, so hook-related content is removed.
36
+ */
37
+ const CODEX_STRIP_PATTERNS = [
38
+ /## (?:Hooks?|Hook System)[\s\S]*?(?=\n## |\n---|\n$)/gi,
39
+ /- \*\*Hooks?\*\*:.*\n/gi,
40
+ /hook[s]? \(.*?\)/gi,
41
+ ];
42
+
43
+ /**
44
+ * Text replacements for Codex CLI context file.
45
+ */
46
+ const CODEX_REPLACEMENTS = [
47
+ ['Claude Code', 'Codex CLI'],
48
+ ['claude code', 'Codex CLI'],
49
+ ['CLAUDE.md', 'AGENTS.md'],
50
+ ['Claude.md', 'AGENTS.md'],
51
+ ['.claude/commands/', '.codex/agents/'],
52
+ ['.claude/rules/', '.codex/rules/'],
53
+ ['.claude/mcp.json', '.codex/mcp.json'],
54
+ ['claude --print', 'codex exec'],
55
+ ['claude -p', 'codex exec'],
56
+ ['CLAUDE.local.md', 'AGENTS.local.md'],
57
+ ];
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Generators
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Generate GEMINI.md content from CLAUDE.md content.
65
+ *
66
+ * Adapts the content by replacing Claude Code-specific references with
67
+ * Gemini CLI equivalents. Preserves structure and formatting.
68
+ *
69
+ * @param {string} content - Raw CLAUDE.md content
70
+ * @returns {string} Adapted content for GEMINI.md
71
+ */
72
+ export function generateGeminiMd(content) {
73
+ let result = content;
74
+
75
+ for (const [search, replace] of GEMINI_REPLACEMENTS) {
76
+ result = result.replaceAll(search, replace);
77
+ }
78
+
79
+ // Prepend a header noting this file is auto-generated
80
+ const header = [
81
+ '<!-- Auto-generated by chati.dev from CLAUDE.md — do not edit manually -->',
82
+ '',
83
+ ].join('\n');
84
+
85
+ return header + result;
86
+ }
87
+
88
+ /**
89
+ * Generate AGENTS.md content from CLAUDE.md content.
90
+ *
91
+ * Simplifies the content for Codex CLI: strips hook references,
92
+ * replaces CLI-specific paths, and produces a leaner context file
93
+ * focused on code execution.
94
+ *
95
+ * @param {string} content - Raw CLAUDE.md content
96
+ * @returns {string} Adapted content for AGENTS.md
97
+ */
98
+ export function generateAgentsMd(content) {
99
+ let result = content;
100
+
101
+ // Strip hook-related sections (Codex has no hooks support)
102
+ for (const pattern of CODEX_STRIP_PATTERNS) {
103
+ result = result.replace(pattern, '');
104
+ }
105
+
106
+ // Apply text replacements
107
+ for (const [search, replace] of CODEX_REPLACEMENTS) {
108
+ result = result.replaceAll(search, replace);
109
+ }
110
+
111
+ // Clean up any double blank lines left by stripping
112
+ result = result.replace(/\n{3,}/g, '\n\n');
113
+
114
+ // Prepend a header noting this file is auto-generated
115
+ const header = [
116
+ '<!-- Auto-generated by chati.dev from CLAUDE.md — do not edit manually -->',
117
+ '',
118
+ ].join('\n');
119
+
120
+ return header + result;
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Orchestrator
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Generate context files for all enabled alternative providers.
129
+ *
130
+ * Reads config.yaml to determine which providers are enabled, reads
131
+ * CLAUDE.md from the project root, and writes the appropriate context
132
+ * files (GEMINI.md, AGENTS.md) when their providers are active.
133
+ *
134
+ * @param {string} projectDir - Project root directory
135
+ * @returns {{ generated: string[], skipped: string[], warning: string|null }}
136
+ */
137
+ export function generateContextFiles(projectDir) {
138
+ const result = { generated: [], skipped: [], warning: null };
139
+
140
+ // Read CLAUDE.md
141
+ const claudeMdPath = join(projectDir, 'CLAUDE.md');
142
+ if (!existsSync(claudeMdPath)) {
143
+ result.warning = 'CLAUDE.md not found — skipping context file generation';
144
+ return result;
145
+ }
146
+
147
+ const claudeContent = readFileSync(claudeMdPath, 'utf-8');
148
+
149
+ // Determine enabled providers from config.yaml
150
+ const enabledProviders = resolveEnabledProviders(projectDir);
151
+
152
+ // Provider-to-generator mapping
153
+ const generators = {
154
+ gemini: {
155
+ filename: 'GEMINI.md',
156
+ generate: generateGeminiMd,
157
+ },
158
+ codex: {
159
+ filename: 'AGENTS.md',
160
+ generate: generateAgentsMd,
161
+ },
162
+ };
163
+
164
+ for (const [provider, config] of Object.entries(generators)) {
165
+ if (enabledProviders.includes(provider)) {
166
+ const outputPath = join(projectDir, config.filename);
167
+ const content = config.generate(claudeContent);
168
+ writeFileSync(outputPath, content, 'utf-8');
169
+ result.generated.push(config.filename);
170
+ } else {
171
+ result.skipped.push(config.filename);
172
+ }
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Helpers
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /**
183
+ * Resolve which alternative providers are enabled from config.yaml.
184
+ *
185
+ * Uses lightweight regex-based YAML extraction (no dependency on a
186
+ * full YAML parser) consistent with cli-registry.js patterns.
187
+ *
188
+ * @param {string} projectDir - Project root directory
189
+ * @returns {string[]} List of enabled provider names
190
+ */
191
+ function resolveEnabledProviders(projectDir) {
192
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
193
+ if (!existsSync(configPath)) {
194
+ return [];
195
+ }
196
+
197
+ const raw = readFileSync(configPath, 'utf-8');
198
+ const providers = ['gemini', 'codex', 'copilot'];
199
+ const enabled = [];
200
+
201
+ for (const name of providers) {
202
+ const match = raw.match(new RegExp(`${name}:[\\s\\S]*?enabled:\\s*(true|false)`, 'm'));
203
+ if (match && match[1] === 'true') {
204
+ enabled.push(name);
205
+ }
206
+ }
207
+
208
+ return enabled;
209
+ }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { existsSync, readdirSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
- import { GateBase } from './gate-base.js';
10
+ import { GateBase, GateVerdict, determineVerdict } from './gate-base.js';
11
11
  import { loadSession } from '../orchestrator/session-manager.js';
12
12
  import { loadHandoff } from '../tasks/handoff.js';
13
13
 
@@ -148,6 +148,8 @@ export class QAPlanningGate extends GateBase {
148
148
  ? Math.round((criteriaResults.length / allCriteria.length) * 100)
149
149
  : 0;
150
150
 
151
- return { score, criteriaResults, allCriteria, warnings };
151
+ const verdict = determineVerdict(score, 95);
152
+
153
+ return { score, criteriaResults, allCriteria, warnings, verdict };
152
154
  }
153
155
  }
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { existsSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
- import { GateBase } from './gate-base.js';
17
+ import { GateBase, GateVerdict, determineVerdict } from './gate-base.js';
18
18
  import { loadSession } from '../orchestrator/session-manager.js';
19
19
  import { loadHandoff } from '../tasks/handoff.js';
20
20
 
@@ -202,6 +202,9 @@ export class QAImplementationGate extends GateBase {
202
202
  ? Math.round((criteriaResults.length / allCriteria.length) * 100)
203
203
  : 0;
204
204
 
205
- return { score, criteriaResults, allCriteria, warnings };
205
+ const hasCriticalBlocker = !evidence.noCriticalBugs;
206
+ const verdict = determineVerdict(score, 95, hasCriticalBlocker);
207
+
208
+ return { score, criteriaResults, allCriteria, warnings, verdict };
206
209
  }
207
210
  }
@@ -8,6 +8,33 @@
8
8
 
9
9
  import { evaluateGate, getGateThreshold, resolveGateAction } from '../autonomy/autonomous-gate.js';
10
10
 
11
+ /**
12
+ * Gate verdict constants (v3.0.0).
13
+ * APPROVED: Score >= threshold — proceed to next pipeline stage.
14
+ * NEEDS_REVISION: Score within 5 points below threshold — return to agent for fixes.
15
+ * BLOCKED: Score significantly below threshold or critical blocker — halt pipeline.
16
+ */
17
+ export const GateVerdict = {
18
+ APPROVED: 'approved',
19
+ NEEDS_REVISION: 'needs_revision',
20
+ BLOCKED: 'blocked',
21
+ };
22
+
23
+ /**
24
+ * Determine the gate verdict from a score and threshold.
25
+ *
26
+ * @param {number} score - Gate evaluation score (0-100)
27
+ * @param {number} threshold - Minimum score to pass
28
+ * @param {boolean} [hasCriticalBlocker=false] - Whether a critical blocker exists
29
+ * @returns {string} One of GateVerdict values
30
+ */
31
+ export function determineVerdict(score, threshold, hasCriticalBlocker = false) {
32
+ if (hasCriticalBlocker) return GateVerdict.BLOCKED;
33
+ if (score >= threshold) return GateVerdict.APPROVED;
34
+ if (score >= threshold - 5) return GateVerdict.NEEDS_REVISION;
35
+ return GateVerdict.BLOCKED;
36
+ }
37
+
11
38
  /**
12
39
  * Abstract base class for quality gates.
13
40
  *
@@ -70,14 +97,19 @@ export class GateBase {
70
97
 
71
98
  const action = resolveGateAction(gateResult.result, mode);
72
99
 
100
+ const threshold = getGateThreshold(this.agent);
101
+ const hasCriticalBlocker = warnings.some(w => w.toLowerCase().includes('critical'));
102
+ const verdict = determineVerdict(gateResult.score, threshold, hasCriticalBlocker);
103
+
73
104
  return {
74
105
  gateId: this.id,
75
106
  gateName: this.name,
76
107
  result: gateResult.result,
108
+ verdict,
77
109
  score: gateResult.score,
78
110
  evidence,
79
111
  recommendation: action.action,
80
- canProceed: gateResult.canProceed,
112
+ canProceed: verdict === GateVerdict.APPROVED,
81
113
  details: gateResult.details,
82
114
  warnings,
83
115
  };
@@ -0,0 +1,250 @@
1
+ /**
2
+ * @fileoverview Health check engine for chati.dev.
3
+ *
4
+ * Validates framework integrity, CLI availability, session state,
5
+ * hook health, dependencies, and provider authentication.
6
+ * Constitution Article XIV — Framework Registry Governance.
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Check Result Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * @typedef {object} CheckResult
18
+ * @property {string} name - Check name
19
+ * @property {'pass'|'warn'|'fail'} status - Check status
20
+ * @property {string} message - Human-readable result
21
+ * @property {number} duration - Execution time in ms
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} HealthReport
26
+ * @property {number} score - Overall score (0-100)
27
+ * @property {number} total - Total checks run
28
+ * @property {number} passed - Checks passed
29
+ * @property {number} warned - Checks with warnings
30
+ * @property {number} failed - Checks failed
31
+ * @property {CheckResult[]} checks - Individual check results
32
+ * @property {string} timestamp - ISO timestamp
33
+ */
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Individual Checks
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Check if configured CLI providers are available on the system.
41
+ *
42
+ * @param {string} projectDir
43
+ * @returns {Promise<CheckResult>}
44
+ */
45
+ export async function checkCliAvailability(projectDir) {
46
+ const start = Date.now();
47
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
48
+
49
+ if (!existsSync(configPath)) {
50
+ return { name: 'cli-availability', status: 'warn', message: 'No config.yaml found', duration: Date.now() - start };
51
+ }
52
+
53
+ const raw = readFileSync(configPath, 'utf-8');
54
+ const providers = ['claude', 'gemini', 'codex', 'copilot'];
55
+ const missing = [];
56
+
57
+ for (const provider of providers) {
58
+ const enabledMatch = raw.match(new RegExp(`${provider}:[\\s\\S]*?enabled:\\s*true`, 'm'));
59
+ if (enabledMatch) {
60
+ const { execSync } = await import('child_process');
61
+ try {
62
+ execSync(`which ${provider}`, { stdio: 'ignore' });
63
+ } catch {
64
+ missing.push(provider);
65
+ }
66
+ }
67
+ }
68
+
69
+ if (missing.length > 0) {
70
+ return { name: 'cli-availability', status: 'warn', message: `Missing CLIs: ${missing.join(', ')}`, duration: Date.now() - start };
71
+ }
72
+
73
+ return { name: 'cli-availability', status: 'pass', message: 'All enabled CLIs available', duration: Date.now() - start };
74
+ }
75
+
76
+ /**
77
+ * Validate framework file integrity against entity registry.
78
+ *
79
+ * @param {string} projectDir
80
+ * @returns {Promise<CheckResult>}
81
+ */
82
+ export async function checkFrameworkIntegrity(projectDir) {
83
+ const start = Date.now();
84
+ const registryPath = join(projectDir, 'chati.dev', 'data', 'entity-registry.yaml');
85
+
86
+ if (!existsSync(registryPath)) {
87
+ return { name: 'framework-integrity', status: 'warn', message: 'No entity registry found', duration: Date.now() - start };
88
+ }
89
+
90
+ const raw = readFileSync(registryPath, 'utf-8');
91
+ const pathMatches = raw.match(/path:\s*["']?([^\s"']+)["']?/gm) || [];
92
+ let missing = 0;
93
+
94
+ for (const match of pathMatches) {
95
+ const filePath = match.replace(/path:\s*["']?/, '').replace(/["']?$/, '');
96
+ if (!existsSync(join(projectDir, filePath))) {
97
+ missing++;
98
+ }
99
+ }
100
+
101
+ if (missing > 0) {
102
+ return { name: 'framework-integrity', status: 'warn', message: `${missing} registered entities missing from filesystem`, duration: Date.now() - start };
103
+ }
104
+
105
+ return { name: 'framework-integrity', status: 'pass', message: 'All registered entities exist', duration: Date.now() - start };
106
+ }
107
+
108
+ /**
109
+ * Validate session.yaml structure.
110
+ *
111
+ * @param {string} projectDir
112
+ * @returns {Promise<CheckResult>}
113
+ */
114
+ export async function checkSessionState(projectDir) {
115
+ const start = Date.now();
116
+ const sessionPath = join(projectDir, '.chati', 'session.yaml');
117
+
118
+ if (!existsSync(sessionPath)) {
119
+ return { name: 'session-state', status: 'pass', message: 'No active session (expected for new projects)', duration: Date.now() - start };
120
+ }
121
+
122
+ const raw = readFileSync(sessionPath, 'utf-8');
123
+ const requiredFields = ['project', 'language'];
124
+ const missingFields = requiredFields.filter((f) => !raw.includes(`${f}:`));
125
+
126
+ if (missingFields.length > 0) {
127
+ return { name: 'session-state', status: 'warn', message: `Session missing fields: ${missingFields.join(', ')}`, duration: Date.now() - start };
128
+ }
129
+
130
+ return { name: 'session-state', status: 'pass', message: 'Session state valid', duration: Date.now() - start };
131
+ }
132
+
133
+ /**
134
+ * Verify that hooks are functional.
135
+ *
136
+ * @param {string} projectDir
137
+ * @returns {Promise<CheckResult>}
138
+ */
139
+ export async function checkHooksHealth(projectDir) {
140
+ const start = Date.now();
141
+ const hooksDir = join(projectDir, 'chati.dev', 'hooks');
142
+
143
+ if (!existsSync(hooksDir)) {
144
+ return { name: 'hooks-health', status: 'fail', message: 'Hooks directory not found', duration: Date.now() - start };
145
+ }
146
+
147
+ const expectedHooks = ['prism-engine.js', 'model-governance.js', 'mode-governance.js', 'constitution-guard.js', 'read-protection.js', 'session-digest.js'];
148
+ const missing = expectedHooks.filter((h) => !existsSync(join(hooksDir, h)));
149
+
150
+ if (missing.length > 0) {
151
+ return { name: 'hooks-health', status: 'fail', message: `Missing hooks: ${missing.join(', ')}`, duration: Date.now() - start };
152
+ }
153
+
154
+ return { name: 'hooks-health', status: 'pass', message: 'All 6 hooks present', duration: Date.now() - start };
155
+ }
156
+
157
+ /**
158
+ * Verify system dependencies.
159
+ *
160
+ * @returns {Promise<CheckResult>}
161
+ */
162
+ export async function checkDependencies() {
163
+ const start = Date.now();
164
+ const issues = [];
165
+
166
+ const { execSync } = await import('child_process');
167
+
168
+ // Check Node.js version
169
+ try {
170
+ const nodeVersion = execSync('node --version', { encoding: 'utf-8' }).trim();
171
+ const major = parseInt(nodeVersion.replace('v', '').split('.')[0], 10);
172
+ if (major < 20) {
173
+ issues.push(`Node.js ${nodeVersion} < required v20`);
174
+ }
175
+ } catch {
176
+ issues.push('Node.js not found');
177
+ }
178
+
179
+ // Check git
180
+ try {
181
+ execSync('git --version', { stdio: 'ignore' });
182
+ } catch {
183
+ issues.push('git not found');
184
+ }
185
+
186
+ if (issues.length > 0) {
187
+ return { name: 'dependencies', status: 'fail', message: issues.join('; '), duration: Date.now() - start };
188
+ }
189
+
190
+ return { name: 'dependencies', status: 'pass', message: 'All dependencies satisfied', duration: Date.now() - start };
191
+ }
192
+
193
+ /**
194
+ * Check git repository status.
195
+ *
196
+ * @param {string} projectDir
197
+ * @returns {Promise<CheckResult>}
198
+ */
199
+ export async function checkGitStatus(projectDir) {
200
+ const start = Date.now();
201
+
202
+ if (!existsSync(join(projectDir, '.git'))) {
203
+ return { name: 'git-status', status: 'warn', message: 'Not a git repository', duration: Date.now() - start };
204
+ }
205
+
206
+ return { name: 'git-status', status: 'pass', message: 'Git repository detected', duration: Date.now() - start };
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Health Engine
211
+ // ---------------------------------------------------------------------------
212
+
213
+ /**
214
+ * Run all health checks and produce a report.
215
+ *
216
+ * @param {string} projectDir - Project root directory
217
+ * @returns {Promise<HealthReport>}
218
+ */
219
+ export async function runHealthChecks(projectDir) {
220
+ const checks = [
221
+ checkCliAvailability(projectDir),
222
+ checkFrameworkIntegrity(projectDir),
223
+ checkSessionState(projectDir),
224
+ checkHooksHealth(projectDir),
225
+ checkDependencies(),
226
+ checkGitStatus(projectDir),
227
+ ];
228
+
229
+ const results = await Promise.allSettled(checks);
230
+ const checkResults = results.map((r) => {
231
+ if (r.status === 'fulfilled') return r.value;
232
+ return { name: 'unknown', status: 'fail', message: r.reason?.message || 'Check failed', duration: 0 };
233
+ });
234
+
235
+ const passed = checkResults.filter((c) => c.status === 'pass').length;
236
+ const warned = checkResults.filter((c) => c.status === 'warn').length;
237
+ const failed = checkResults.filter((c) => c.status === 'fail').length;
238
+ const total = checkResults.length;
239
+ const score = Math.round((passed / total) * 100);
240
+
241
+ return {
242
+ score,
243
+ total,
244
+ passed,
245
+ warned,
246
+ failed,
247
+ checks: checkResults,
248
+ timestamp: new Date().toISOString(),
249
+ };
250
+ }
@@ -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
+ }