chati-dev 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/logo.svg +42 -0
- package/assets/logo.txt +3 -3
- package/bin/chati.js +148 -8
- package/framework/constitution.md +91 -5
- package/framework/data/entity-registry.yaml +399 -0
- package/framework/i18n/en.yaml +9 -4
- package/framework/i18n/es.yaml +9 -4
- package/framework/i18n/fr.yaml +9 -4
- package/framework/i18n/pt.yaml +9 -4
- package/framework/intelligence/context-engine.md +163 -0
- package/framework/intelligence/decision-engine.md +134 -0
- package/framework/intelligence/memory-layer.md +187 -0
- package/framework/migrations/v1.1-to-v1.2.yaml +145 -0
- package/framework/orchestrator/chati.md +154 -8
- package/framework/schemas/config.schema.json +1 -1
- package/framework/schemas/context.schema.json +98 -0
- package/framework/schemas/memory.schema.json +79 -0
- package/package.json +14 -7
- package/scripts/bundle-framework.js +1 -1
- package/src/dashboard/data-reader.js +27 -2
- package/src/dashboard/layout.js +21 -0
- package/src/dashboard/renderer.js +11 -8
- package/src/installer/core.js +33 -6
- package/src/installer/templates.js +7 -0
- package/src/installer/validator.js +44 -5
- package/src/intelligence/context-status.js +80 -0
- package/src/intelligence/memory-manager.js +188 -0
- package/src/intelligence/registry-manager.js +202 -0
- package/src/upgrade/backup.js +2 -0
- package/src/upgrade/migrator.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/wizard/i18n.js +9 -4
- package/src/wizard/index.js +5 -0
package/src/installer/core.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, copyFileSync, existsSync
|
|
1
|
+
import { mkdirSync, writeFileSync, copyFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import yaml from 'js-yaml';
|
|
5
4
|
import { IDE_CONFIGS } from '../config/ide-configs.js';
|
|
6
5
|
import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
|
|
7
6
|
import { generateSessionYaml, generateConfigYaml, generateClaudeMd } from './templates.js';
|
|
@@ -38,7 +37,7 @@ export async function installFramework(config) {
|
|
|
38
37
|
'agents/clarity', 'agents/quality', 'agents/build', 'agents/deploy',
|
|
39
38
|
'templates', 'workflows', 'quality-gates',
|
|
40
39
|
'schemas', 'frameworks', 'intelligence', 'patterns',
|
|
41
|
-
'i18n', 'migrations',
|
|
40
|
+
'i18n', 'migrations', 'data',
|
|
42
41
|
'artifacts/0-WU', 'artifacts/1-Brief', 'artifacts/2-PRD',
|
|
43
42
|
'artifacts/3-Architecture', 'artifacts/4-UX', 'artifacts/5-Phases',
|
|
44
43
|
'artifacts/6-Tasks', 'artifacts/7-QA-Planning', 'artifacts/8-Validation',
|
|
@@ -49,6 +48,27 @@ export async function installFramework(config) {
|
|
|
49
48
|
createDir(join(frameworkDir, dir));
|
|
50
49
|
}
|
|
51
50
|
|
|
51
|
+
// Create .chati/memories/ directory tree for Memory Layer
|
|
52
|
+
const memoriesBase = join(targetDir, '.chati', 'memories');
|
|
53
|
+
const memoryDirs = [
|
|
54
|
+
'shared/durable', 'shared/daily', 'shared/session',
|
|
55
|
+
'greenfield-wu/durable', 'greenfield-wu/daily',
|
|
56
|
+
'brownfield-wu/durable', 'brownfield-wu/daily',
|
|
57
|
+
'brief/durable', 'brief/daily',
|
|
58
|
+
'detail/durable', 'detail/daily',
|
|
59
|
+
'architect/durable', 'architect/daily',
|
|
60
|
+
'ux/durable', 'ux/daily',
|
|
61
|
+
'phases/durable', 'phases/daily',
|
|
62
|
+
'tasks/durable', 'tasks/daily',
|
|
63
|
+
'qa-planning/durable', 'qa-planning/daily',
|
|
64
|
+
'qa-implementation/durable', 'qa-implementation/daily',
|
|
65
|
+
'dev/durable', 'dev/daily',
|
|
66
|
+
'devops/durable', 'devops/daily',
|
|
67
|
+
];
|
|
68
|
+
for (const dir of memoryDirs) {
|
|
69
|
+
createDir(join(memoriesBase, dir));
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
// Copy framework files from source
|
|
53
73
|
copyFrameworkFiles(frameworkDir);
|
|
54
74
|
|
|
@@ -115,6 +135,8 @@ function copyFrameworkFiles(destDir) {
|
|
|
115
135
|
'schemas/session.schema.json',
|
|
116
136
|
'schemas/config.schema.json',
|
|
117
137
|
'schemas/task.schema.json',
|
|
138
|
+
'schemas/context.schema.json',
|
|
139
|
+
'schemas/memory.schema.json',
|
|
118
140
|
// Frameworks
|
|
119
141
|
'frameworks/quality-dimensions.yaml',
|
|
120
142
|
'frameworks/decision-heuristics.yaml',
|
|
@@ -122,6 +144,9 @@ function copyFrameworkFiles(destDir) {
|
|
|
122
144
|
'intelligence/gotchas.yaml',
|
|
123
145
|
'intelligence/patterns.yaml',
|
|
124
146
|
'intelligence/confidence.yaml',
|
|
147
|
+
'intelligence/context-engine.md',
|
|
148
|
+
'intelligence/memory-layer.md',
|
|
149
|
+
'intelligence/decision-engine.md',
|
|
125
150
|
// Patterns
|
|
126
151
|
'patterns/elicitation.md',
|
|
127
152
|
// i18n
|
|
@@ -131,6 +156,8 @@ function copyFrameworkFiles(destDir) {
|
|
|
131
156
|
'i18n/fr.yaml',
|
|
132
157
|
// Migrations
|
|
133
158
|
'migrations/v1.0-to-v1.1.yaml',
|
|
159
|
+
// Data
|
|
160
|
+
'data/entity-registry.yaml',
|
|
134
161
|
];
|
|
135
162
|
|
|
136
163
|
for (const file of filesToCopy) {
|
|
@@ -182,11 +209,11 @@ This is a thin router. All logic lives in the orchestrator.
|
|
|
182
209
|
}
|
|
183
210
|
} else {
|
|
184
211
|
// For other IDEs, create a rules file pointing to chati.dev/
|
|
185
|
-
const rulesContent = `# chati.dev
|
|
212
|
+
const rulesContent = `# chati.dev System Rules
|
|
186
213
|
# This file configures ${config.name} to work with chati.dev
|
|
187
214
|
|
|
188
|
-
##
|
|
189
|
-
All
|
|
215
|
+
## System Location
|
|
216
|
+
All system content is in the \`chati.dev/\` directory.
|
|
190
217
|
|
|
191
218
|
## Session State
|
|
192
219
|
Runtime session state is in \`.chati/session.yaml\` (IDE-agnostic).
|
|
@@ -80,6 +80,13 @@ export function generateClaudeMd(config) {
|
|
|
80
80
|
## Quick Start
|
|
81
81
|
Type \`/chati\` to activate the orchestrator. It will guide you through the entire process.
|
|
82
82
|
|
|
83
|
+
## Session Lock
|
|
84
|
+
**Status: INACTIVE** — Type \`/chati\` to activate.
|
|
85
|
+
|
|
86
|
+
When active, ALL messages are routed through the chati.dev orchestrator. The user stays inside the system until they explicitly exit with \`/chati exit\`.
|
|
87
|
+
|
|
88
|
+
<!-- SESSION-LOCK:INACTIVE -->
|
|
89
|
+
|
|
83
90
|
## Key Files
|
|
84
91
|
- **Session**: \`.chati/session.yaml\` (runtime state)
|
|
85
92
|
- **Constitution**: \`chati.dev/constitution.md\` (governance)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -13,6 +13,9 @@ export async function validateInstallation(targetDir) {
|
|
|
13
13
|
schemas: { pass: false, details: [] },
|
|
14
14
|
workflows: { pass: false, details: [] },
|
|
15
15
|
templates: { pass: false, details: [] },
|
|
16
|
+
intelligence: { pass: false, details: [] },
|
|
17
|
+
registry: { pass: false, details: [] },
|
|
18
|
+
memories: { pass: false, details: [] },
|
|
16
19
|
total: 0,
|
|
17
20
|
passed: 0,
|
|
18
21
|
};
|
|
@@ -56,7 +59,7 @@ export async function validateInstallation(targetDir) {
|
|
|
56
59
|
if (existsSync(constitutionPath)) {
|
|
57
60
|
const content = readFileSync(constitutionPath, 'utf-8');
|
|
58
61
|
const articleCount = (content.match(/^## Article/gm) || []).length;
|
|
59
|
-
results.constitution.pass = articleCount >=
|
|
62
|
+
results.constitution.pass = articleCount >= 15;
|
|
60
63
|
results.constitution.details.push({ articleCount });
|
|
61
64
|
}
|
|
62
65
|
results.total += 1;
|
|
@@ -68,15 +71,18 @@ export async function validateInstallation(targetDir) {
|
|
|
68
71
|
results.total += 1;
|
|
69
72
|
if (results.session.pass) results.passed += 1;
|
|
70
73
|
|
|
71
|
-
// Check schemas
|
|
72
|
-
const schemaFiles = [
|
|
74
|
+
// Check schemas (5 total: session, config, task, context, memory)
|
|
75
|
+
const schemaFiles = [
|
|
76
|
+
'session.schema.json', 'config.schema.json', 'task.schema.json',
|
|
77
|
+
'context.schema.json', 'memory.schema.json',
|
|
78
|
+
];
|
|
73
79
|
let schemaCount = 0;
|
|
74
80
|
for (const file of schemaFiles) {
|
|
75
81
|
if (existsSync(join(targetDir, 'chati.dev', 'schemas', file))) {
|
|
76
82
|
schemaCount++;
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
|
-
results.schemas.pass = schemaCount ===
|
|
85
|
+
results.schemas.pass = schemaCount === 5;
|
|
80
86
|
results.total += 1;
|
|
81
87
|
if (results.schemas.pass) results.passed += 1;
|
|
82
88
|
|
|
@@ -110,5 +116,38 @@ export async function validateInstallation(targetDir) {
|
|
|
110
116
|
results.total += 1;
|
|
111
117
|
if (results.templates.pass) results.passed += 1;
|
|
112
118
|
|
|
119
|
+
// Check intelligence files (6: 3 specs + 3 yaml)
|
|
120
|
+
const intelligenceFiles = [
|
|
121
|
+
'intelligence/context-engine.md',
|
|
122
|
+
'intelligence/memory-layer.md',
|
|
123
|
+
'intelligence/decision-engine.md',
|
|
124
|
+
'intelligence/gotchas.yaml',
|
|
125
|
+
'intelligence/patterns.yaml',
|
|
126
|
+
'intelligence/confidence.yaml',
|
|
127
|
+
];
|
|
128
|
+
let intelCount = 0;
|
|
129
|
+
for (const file of intelligenceFiles) {
|
|
130
|
+
if (existsSync(join(targetDir, 'chati.dev', file))) {
|
|
131
|
+
intelCount++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
results.intelligence.pass = intelCount === 6;
|
|
135
|
+
results.intelligence.details.push({ found: intelCount, expected: 6 });
|
|
136
|
+
results.total += 1;
|
|
137
|
+
if (results.intelligence.pass) results.passed += 1;
|
|
138
|
+
|
|
139
|
+
// Check entity registry
|
|
140
|
+
const registryPath = join(targetDir, 'chati.dev', 'data', 'entity-registry.yaml');
|
|
141
|
+
results.registry.pass = existsSync(registryPath);
|
|
142
|
+
results.total += 1;
|
|
143
|
+
if (results.registry.pass) results.passed += 1;
|
|
144
|
+
|
|
145
|
+
// Check .chati/memories/ directory tree
|
|
146
|
+
const memoriesPath = join(targetDir, '.chati', 'memories');
|
|
147
|
+
const memoriesShared = join(memoriesPath, 'shared', 'durable');
|
|
148
|
+
results.memories.pass = existsSync(memoriesPath) && existsSync(memoriesShared);
|
|
149
|
+
results.total += 1;
|
|
150
|
+
if (results.memories.pass) results.passed += 1;
|
|
151
|
+
|
|
113
152
|
return results;
|
|
114
153
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const BRACKETS = [
|
|
5
|
+
{ name: 'FRESH', min: 60, max: 100, layers: ['L0', 'L1', 'L2', 'L3', 'L4'], budget: 2500 },
|
|
6
|
+
{ name: 'MODERATE', min: 40, max: 60, layers: ['L0', 'L1', 'L2', 'L3', 'L4'], budget: 2000 },
|
|
7
|
+
{ name: 'DEPLETED', min: 25, max: 40, layers: ['L0', 'L1', 'L2'], budget: 1500 },
|
|
8
|
+
{ name: 'CRITICAL', min: 0, max: 25, layers: ['L0', 'L1'], budget: 800 },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get context status based on session state
|
|
13
|
+
* (Advisory — actual bracket is calculated at runtime by the orchestrator)
|
|
14
|
+
*/
|
|
15
|
+
export function getContextStatus(targetDir) {
|
|
16
|
+
const sessionPath = join(targetDir, '.chati', 'session.yaml');
|
|
17
|
+
if (!existsSync(sessionPath)) {
|
|
18
|
+
return {
|
|
19
|
+
bracket: 'FRESH',
|
|
20
|
+
activeLayers: BRACKETS[0].layers,
|
|
21
|
+
tokenBudget: BRACKETS[0].budget,
|
|
22
|
+
memoryLevel: 'none',
|
|
23
|
+
advisory: 'No active session — default FRESH bracket',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Read session to determine pipeline state
|
|
28
|
+
const sessionContent = readFileSync(sessionPath, 'utf-8');
|
|
29
|
+
const currentAgent = extractYamlValue(sessionContent, 'current_agent');
|
|
30
|
+
const state = extractYamlValue(sessionContent, 'state');
|
|
31
|
+
|
|
32
|
+
// Count completed agents as a rough proxy for context usage
|
|
33
|
+
const completedCount = (sessionContent.match(/status: completed/g) || []).length;
|
|
34
|
+
const inProgressCount = (sessionContent.match(/status: in_progress/g) || []).length;
|
|
35
|
+
|
|
36
|
+
// Estimate context usage based on pipeline progress
|
|
37
|
+
// More agents completed = more context consumed
|
|
38
|
+
const estimatedUsage = Math.min(95, completedCount * 8 + inProgressCount * 5);
|
|
39
|
+
const remainingPercent = 100 - estimatedUsage;
|
|
40
|
+
|
|
41
|
+
const bracket = BRACKETS.find(b => remainingPercent >= b.min && remainingPercent <= b.max) || BRACKETS[0];
|
|
42
|
+
|
|
43
|
+
const memoryLevels = { FRESH: 'none', MODERATE: 'metadata', DEPLETED: 'chunks', CRITICAL: 'full' };
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
bracket: bracket.name,
|
|
47
|
+
activeLayers: bracket.layers,
|
|
48
|
+
tokenBudget: bracket.budget,
|
|
49
|
+
memoryLevel: memoryLevels[bracket.name],
|
|
50
|
+
remainingPercent,
|
|
51
|
+
estimatedUsage,
|
|
52
|
+
currentAgent: currentAgent || 'none',
|
|
53
|
+
pipelineState: state || 'unknown',
|
|
54
|
+
completedAgents: completedCount,
|
|
55
|
+
advisory: `Estimated bracket based on pipeline progress (${completedCount} agents completed)`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Estimate context usage for display purposes
|
|
61
|
+
*/
|
|
62
|
+
export function estimateContextUsage(targetDir) {
|
|
63
|
+
const status = getContextStatus(targetDir);
|
|
64
|
+
return {
|
|
65
|
+
bracket: status.bracket,
|
|
66
|
+
remainingPercent: status.remainingPercent,
|
|
67
|
+
tokenBudget: status.tokenBudget,
|
|
68
|
+
layers: status.activeLayers,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Simple YAML value extractor (avoids js-yaml dependency)
|
|
74
|
+
*/
|
|
75
|
+
function extractYamlValue(content, key) {
|
|
76
|
+
const regex = new RegExp(`^\\s*${key}:\\s*(.+)$`, 'm');
|
|
77
|
+
const match = content.match(regex);
|
|
78
|
+
if (!match) return null;
|
|
79
|
+
return match[1].trim().replace(/^["']|["']$/g, '');
|
|
80
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
const MEMORIES_DIR = '.chati/memories';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* List memories with optional filters
|
|
8
|
+
*/
|
|
9
|
+
export function listMemories(targetDir, options = {}) {
|
|
10
|
+
const memoriesPath = join(targetDir, MEMORIES_DIR);
|
|
11
|
+
if (!existsSync(memoriesPath)) return [];
|
|
12
|
+
|
|
13
|
+
const memories = [];
|
|
14
|
+
walkMemories(memoriesPath, (filePath) => {
|
|
15
|
+
const meta = parseMemoryFrontmatter(filePath);
|
|
16
|
+
if (!meta) return;
|
|
17
|
+
|
|
18
|
+
if (options.agent && meta.agent !== options.agent) return;
|
|
19
|
+
if (options.sector && meta.sector !== options.sector) return;
|
|
20
|
+
if (options.tier && meta.tier !== options.tier) return;
|
|
21
|
+
|
|
22
|
+
memories.push({
|
|
23
|
+
...meta,
|
|
24
|
+
path: relative(memoriesPath, filePath),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return memories;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Search memories by query (matches tags and content)
|
|
33
|
+
*/
|
|
34
|
+
export function searchMemories(targetDir, query) {
|
|
35
|
+
const memoriesPath = join(targetDir, MEMORIES_DIR);
|
|
36
|
+
if (!existsSync(memoriesPath)) return [];
|
|
37
|
+
|
|
38
|
+
const queryLower = query.toLowerCase();
|
|
39
|
+
const results = [];
|
|
40
|
+
|
|
41
|
+
walkMemories(memoriesPath, (filePath) => {
|
|
42
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
43
|
+
const meta = parseMemoryFrontmatter(filePath);
|
|
44
|
+
if (!meta) return;
|
|
45
|
+
|
|
46
|
+
const tagMatch = meta.tags && meta.tags.some(t => t.toLowerCase().includes(queryLower));
|
|
47
|
+
const contentMatch = content.toLowerCase().includes(queryLower);
|
|
48
|
+
|
|
49
|
+
if (tagMatch || contentMatch) {
|
|
50
|
+
results.push({
|
|
51
|
+
...meta,
|
|
52
|
+
path: relative(memoriesPath, filePath),
|
|
53
|
+
matchType: tagMatch ? 'tag' : 'content',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clean expired or cold memories
|
|
63
|
+
*/
|
|
64
|
+
export function cleanMemories(targetDir, options = {}) {
|
|
65
|
+
const memoriesPath = join(targetDir, MEMORIES_DIR);
|
|
66
|
+
if (!existsSync(memoriesPath)) return { cleaned: 0, skipped: 0 };
|
|
67
|
+
|
|
68
|
+
let cleaned = 0;
|
|
69
|
+
let skipped = 0;
|
|
70
|
+
|
|
71
|
+
walkMemories(memoriesPath, (filePath) => {
|
|
72
|
+
const meta = parseMemoryFrontmatter(filePath);
|
|
73
|
+
if (!meta) { skipped++; return; }
|
|
74
|
+
|
|
75
|
+
// Clean session-tier memories (they should be cleaned on new session start)
|
|
76
|
+
const isSessionTier = filePath.includes('/session/');
|
|
77
|
+
|
|
78
|
+
// Clean expired memories
|
|
79
|
+
const isExpired = meta.expires_at && new Date(meta.expires_at) < new Date();
|
|
80
|
+
|
|
81
|
+
if (isSessionTier || isExpired) {
|
|
82
|
+
if (!options.dryRun) {
|
|
83
|
+
unlinkSync(filePath);
|
|
84
|
+
}
|
|
85
|
+
cleaned++;
|
|
86
|
+
} else {
|
|
87
|
+
skipped++;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { cleaned, skipped, dryRun: !!options.dryRun };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get memory statistics
|
|
96
|
+
*/
|
|
97
|
+
export function getMemoryStats(targetDir) {
|
|
98
|
+
const memoriesPath = join(targetDir, MEMORIES_DIR);
|
|
99
|
+
if (!existsSync(memoriesPath)) {
|
|
100
|
+
return { total: 0, byAgent: {}, bySector: {}, byTier: {}, diskUsage: 0 };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stats = {
|
|
104
|
+
total: 0,
|
|
105
|
+
byAgent: {},
|
|
106
|
+
bySector: {},
|
|
107
|
+
byTier: { hot: 0, warm: 0, cold: 0 },
|
|
108
|
+
diskUsage: 0,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
walkMemories(memoriesPath, (filePath) => {
|
|
112
|
+
const meta = parseMemoryFrontmatter(filePath);
|
|
113
|
+
const fileStats = statSync(filePath);
|
|
114
|
+
stats.diskUsage += fileStats.size;
|
|
115
|
+
|
|
116
|
+
if (!meta) return;
|
|
117
|
+
stats.total++;
|
|
118
|
+
|
|
119
|
+
if (meta.agent) {
|
|
120
|
+
stats.byAgent[meta.agent] = (stats.byAgent[meta.agent] || 0) + 1;
|
|
121
|
+
}
|
|
122
|
+
if (meta.sector) {
|
|
123
|
+
stats.bySector[meta.sector] = (stats.bySector[meta.sector] || 0) + 1;
|
|
124
|
+
}
|
|
125
|
+
if (meta.tier && stats.byTier[meta.tier] !== undefined) {
|
|
126
|
+
stats.byTier[meta.tier]++;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return stats;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Walk memory files recursively
|
|
135
|
+
*/
|
|
136
|
+
export function walkMemories(dir, callback) {
|
|
137
|
+
if (!existsSync(dir)) return;
|
|
138
|
+
|
|
139
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const fullPath = join(dir, entry.name);
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
walkMemories(fullPath, callback);
|
|
144
|
+
} else if (entry.name.endsWith('.md')) {
|
|
145
|
+
callback(fullPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse YAML frontmatter from a memory file
|
|
152
|
+
*/
|
|
153
|
+
export function parseMemoryFrontmatter(filePath) {
|
|
154
|
+
try {
|
|
155
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
156
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
157
|
+
if (!fmMatch) return null;
|
|
158
|
+
|
|
159
|
+
// Simple YAML parsing for frontmatter (no dependency on js-yaml)
|
|
160
|
+
const lines = fmMatch[1].split('\n');
|
|
161
|
+
const meta = {};
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
const colonIdx = line.indexOf(':');
|
|
164
|
+
if (colonIdx === -1) continue;
|
|
165
|
+
const key = line.slice(0, colonIdx).trim();
|
|
166
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
167
|
+
|
|
168
|
+
// Handle arrays [a, b, c]
|
|
169
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
170
|
+
value = value.slice(1, -1).split(',').map(s => s.trim());
|
|
171
|
+
}
|
|
172
|
+
// Handle numbers
|
|
173
|
+
else if (!isNaN(value) && value !== '') {
|
|
174
|
+
value = parseFloat(value);
|
|
175
|
+
}
|
|
176
|
+
// Handle quoted strings
|
|
177
|
+
else if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
178
|
+
value = value.slice(1, -1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
meta[key] = value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return meta;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
const REGISTRY_PATH = 'chati.dev/data/entity-registry.yaml';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check registry integrity against filesystem
|
|
9
|
+
*/
|
|
10
|
+
export function checkRegistry(targetDir) {
|
|
11
|
+
const registryPath = join(targetDir, REGISTRY_PATH);
|
|
12
|
+
if (!existsSync(registryPath)) {
|
|
13
|
+
return { valid: false, error: 'Entity registry not found', missing: [], orphaned: [] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const registry = loadRegistry(registryPath);
|
|
17
|
+
if (!registry) {
|
|
18
|
+
return { valid: false, error: 'Failed to parse entity registry', missing: [], orphaned: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const missing = [];
|
|
22
|
+
const found = [];
|
|
23
|
+
|
|
24
|
+
// Check each registered entity exists on disk
|
|
25
|
+
const entities = flattenEntities(registry.entities);
|
|
26
|
+
for (const entity of entities) {
|
|
27
|
+
const filePath = join(targetDir, entity.path);
|
|
28
|
+
if (existsSync(filePath)) {
|
|
29
|
+
found.push(entity);
|
|
30
|
+
} else {
|
|
31
|
+
missing.push(entity);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
valid: missing.length === 0,
|
|
37
|
+
totalEntities: entities.length,
|
|
38
|
+
found: found.length,
|
|
39
|
+
missing,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get registry statistics
|
|
45
|
+
*/
|
|
46
|
+
export function getRegistryStats(targetDir) {
|
|
47
|
+
const registryPath = join(targetDir, REGISTRY_PATH);
|
|
48
|
+
if (!existsSync(registryPath)) {
|
|
49
|
+
return { exists: false, totalEntities: 0, byType: {} };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const registry = loadRegistry(registryPath);
|
|
53
|
+
if (!registry) {
|
|
54
|
+
return { exists: true, totalEntities: 0, byType: {}, error: 'Parse error' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entities = flattenEntities(registry.entities);
|
|
58
|
+
const byType = {};
|
|
59
|
+
for (const entity of entities) {
|
|
60
|
+
const type = entity.type || 'unknown';
|
|
61
|
+
byType[type] = (byType[type] || 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
exists: true,
|
|
66
|
+
version: registry.metadata?.version || 'unknown',
|
|
67
|
+
totalEntities: entities.length,
|
|
68
|
+
declaredCount: registry.metadata?.entity_count || 0,
|
|
69
|
+
countMatch: entities.length === (registry.metadata?.entity_count || 0),
|
|
70
|
+
byType,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate all entities exist on disk
|
|
76
|
+
*/
|
|
77
|
+
export function validateEntities(targetDir) {
|
|
78
|
+
const result = checkRegistry(targetDir);
|
|
79
|
+
return {
|
|
80
|
+
valid: result.valid,
|
|
81
|
+
total: result.totalEntities || 0,
|
|
82
|
+
found: result.found || 0,
|
|
83
|
+
missing: (result.missing || []).map(e => e.path),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run comprehensive health check
|
|
89
|
+
*/
|
|
90
|
+
export function runHealthCheck(targetDir) {
|
|
91
|
+
const checks = {
|
|
92
|
+
registry: { pass: false, details: '' },
|
|
93
|
+
schemas: { pass: false, details: '' },
|
|
94
|
+
constitution: { pass: false, details: '' },
|
|
95
|
+
agents: { pass: false, details: '' },
|
|
96
|
+
entities: { pass: false, details: '' },
|
|
97
|
+
overall: 'UNHEALTHY',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 1. Registry check
|
|
101
|
+
const registryStats = getRegistryStats(targetDir);
|
|
102
|
+
if (registryStats.exists && registryStats.totalEntities > 0) {
|
|
103
|
+
checks.registry.pass = true;
|
|
104
|
+
checks.registry.details = `${registryStats.totalEntities} entities registered (v${registryStats.version})`;
|
|
105
|
+
} else {
|
|
106
|
+
checks.registry.details = registryStats.exists ? 'Registry empty or invalid' : 'Registry not found';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. Schema validation
|
|
110
|
+
const schemaFiles = [
|
|
111
|
+
'session.schema.json', 'config.schema.json', 'task.schema.json',
|
|
112
|
+
'context.schema.json', 'memory.schema.json',
|
|
113
|
+
];
|
|
114
|
+
let validSchemas = 0;
|
|
115
|
+
for (const file of schemaFiles) {
|
|
116
|
+
const schemaPath = join(targetDir, 'chati.dev', 'schemas', file);
|
|
117
|
+
if (existsSync(schemaPath)) {
|
|
118
|
+
try {
|
|
119
|
+
JSON.parse(readFileSync(schemaPath, 'utf-8'));
|
|
120
|
+
validSchemas++;
|
|
121
|
+
} catch {
|
|
122
|
+
// Invalid JSON
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
checks.schemas.pass = validSchemas === schemaFiles.length;
|
|
127
|
+
checks.schemas.details = `${validSchemas}/${schemaFiles.length} valid`;
|
|
128
|
+
|
|
129
|
+
// 3. Constitution check
|
|
130
|
+
const constitutionPath = join(targetDir, 'chati.dev', 'constitution.md');
|
|
131
|
+
if (existsSync(constitutionPath)) {
|
|
132
|
+
const content = readFileSync(constitutionPath, 'utf-8');
|
|
133
|
+
const articleCount = (content.match(/^## Article/gm) || []).length;
|
|
134
|
+
checks.constitution.pass = articleCount >= 15;
|
|
135
|
+
checks.constitution.details = `${articleCount}/15 articles`;
|
|
136
|
+
} else {
|
|
137
|
+
checks.constitution.details = 'Not found';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 4. Agent check
|
|
141
|
+
const agentPaths = [
|
|
142
|
+
'orchestrator/chati.md',
|
|
143
|
+
'agents/clarity/greenfield-wu.md', 'agents/clarity/brownfield-wu.md',
|
|
144
|
+
'agents/clarity/brief.md', 'agents/clarity/detail.md',
|
|
145
|
+
'agents/clarity/architect.md', 'agents/clarity/ux.md',
|
|
146
|
+
'agents/clarity/phases.md', 'agents/clarity/tasks.md',
|
|
147
|
+
'agents/quality/qa-planning.md', 'agents/quality/qa-implementation.md',
|
|
148
|
+
'agents/build/dev.md', 'agents/deploy/devops.md',
|
|
149
|
+
];
|
|
150
|
+
let foundAgents = 0;
|
|
151
|
+
for (const p of agentPaths) {
|
|
152
|
+
if (existsSync(join(targetDir, 'chati.dev', p))) foundAgents++;
|
|
153
|
+
}
|
|
154
|
+
checks.agents.pass = foundAgents === 13;
|
|
155
|
+
checks.agents.details = `${foundAgents}/13 present`;
|
|
156
|
+
|
|
157
|
+
// 5. Entity validation (registry vs filesystem)
|
|
158
|
+
const entityResult = validateEntities(targetDir);
|
|
159
|
+
checks.entities.pass = entityResult.valid;
|
|
160
|
+
checks.entities.details = `${entityResult.found}/${entityResult.total} present`;
|
|
161
|
+
|
|
162
|
+
// Overall
|
|
163
|
+
const passCount = Object.values(checks).filter(c => c && c.pass).length;
|
|
164
|
+
const totalChecks = 5;
|
|
165
|
+
checks.overall = passCount === totalChecks ? 'HEALTHY' : passCount >= 3 ? 'DEGRADED' : 'UNHEALTHY';
|
|
166
|
+
checks.passCount = passCount;
|
|
167
|
+
checks.totalChecks = totalChecks;
|
|
168
|
+
|
|
169
|
+
return checks;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Load and parse registry YAML
|
|
174
|
+
*/
|
|
175
|
+
function loadRegistry(registryPath) {
|
|
176
|
+
try {
|
|
177
|
+
const content = readFileSync(registryPath, 'utf-8');
|
|
178
|
+
return yaml.load(content);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Flatten nested entity categories into flat array
|
|
186
|
+
*/
|
|
187
|
+
function flattenEntities(entities) {
|
|
188
|
+
if (!entities) return [];
|
|
189
|
+
const flat = [];
|
|
190
|
+
|
|
191
|
+
for (const [, items] of Object.entries(entities)) {
|
|
192
|
+
if (typeof items === 'object' && items !== null) {
|
|
193
|
+
for (const [name, entity] of Object.entries(items)) {
|
|
194
|
+
if (entity && entity.path) {
|
|
195
|
+
flat.push({ name, ...entity });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return flat;
|
|
202
|
+
}
|
package/src/upgrade/backup.js
CHANGED
package/src/upgrade/migrator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import semver from 'semver';
|
package/src/utils/logger.js
CHANGED
|
@@ -26,7 +26,7 @@ export function logBanner(logoText, version) {
|
|
|
26
26
|
console.log(brand(line));
|
|
27
27
|
}
|
|
28
28
|
console.log(brand(`chati.dev v${version}`));
|
|
29
|
-
console.log(dim('AI-Powered Multi-Agent
|
|
29
|
+
console.log(dim('AI-Powered Multi-Agent Orchestration System'));
|
|
30
30
|
console.log(dim('═'.repeat(55)));
|
|
31
31
|
console.log();
|
|
32
32
|
}
|