chati-dev 2.1.0 → 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.
- package/README.md +165 -176
- 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 +1 -0
- 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 +66 -1
- package/framework/domains/agents/brownfield-wu.yaml +4 -0
- package/framework/domains/agents/dev.yaml +4 -0
- package/framework/domains/agents/orchestrator.yaml +8 -0
- package/framework/domains/constitution.yaml +28 -0
- package/framework/domains/global.yaml +20 -0
- package/framework/hooks/model-governance.js +17 -15
- package/framework/intelligence/context-engine.md +29 -0
- package/framework/intelligence/memory-layer.md +17 -0
- package/framework/orchestrator/chati.md +94 -14
- package/framework/schemas/config.schema.json +44 -0
- package/framework/schemas/session.schema.json +27 -0
- package/package.json +6 -2
- 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 +209 -0
- 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 +250 -0
- package/src/intelligence/file-tracker.js +117 -0
- package/src/intelligence/timeline.js +144 -0
- package/src/memory/gotchas-auto-capture.js +253 -0
- package/src/terminal/adapters/claude-adapter.js +43 -0
- package/src/terminal/adapters/codex-adapter.js +41 -0
- package/src/terminal/adapters/copilot-adapter.js +38 -0
- package/src/terminal/adapters/gemini-adapter.js +42 -0
- package/src/terminal/adapters/index.js +8 -0
- package/src/terminal/cli-registry.js +218 -0
- package/src/terminal/prompt-builder.js +18 -15
- package/src/terminal/spawner.js +19 -9
- package/src/terminal/wave-analyzer.js +143 -0
- 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,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
|
-
|
|
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
|
-
|
|
205
|
+
const hasCriticalBlocker = !evidence.noCriticalBugs;
|
|
206
|
+
const verdict = determineVerdict(score, 95, hasCriticalBlocker);
|
|
207
|
+
|
|
208
|
+
return { score, criteriaResults, allCriteria, warnings, verdict };
|
|
206
209
|
}
|
|
207
210
|
}
|
package/src/gates/gate-base.js
CHANGED
|
@@ -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:
|
|
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
|
+
}
|