chati-dev 2.0.5 → 2.0.7

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 (128) hide show
  1. package/README.md +51 -38
  2. package/framework/agents/{clarity → planning}/architect.md +1 -1
  3. package/framework/agents/{clarity → planning}/brief.md +1 -1
  4. package/framework/agents/{clarity → planning}/brownfield-wu.md +1 -1
  5. package/framework/agents/{clarity → planning}/detail.md +1 -1
  6. package/framework/agents/{clarity → planning}/greenfield-wu.md +1 -1
  7. package/framework/agents/{clarity → planning}/phases.md +1 -1
  8. package/framework/agents/{clarity → planning}/tasks.md +1 -1
  9. package/framework/agents/{clarity → planning}/ux.md +1 -1
  10. package/framework/agents/quality/qa-planning.md +5 -5
  11. package/framework/constitution.md +7 -7
  12. package/framework/context/governance.md +4 -4
  13. package/framework/context/quality.md +3 -2
  14. package/framework/context/root.md +1 -1
  15. package/framework/data/entity-registry.yaml +9 -9
  16. package/framework/domains/agents/qa-implementation.yaml +1 -1
  17. package/framework/domains/constitution.yaml +2 -2
  18. package/framework/domains/global.yaml +1 -1
  19. package/framework/hooks/mode-governance.js +5 -5
  20. package/framework/hooks/prism-engine.js +22 -2
  21. package/framework/hooks/read-protection.js +107 -0
  22. package/framework/hooks/session-digest.js +1 -1
  23. package/framework/hooks/settings.json +9 -0
  24. package/framework/i18n/en.yaml +1 -3
  25. package/framework/i18n/es.yaml +1 -3
  26. package/framework/i18n/fr.yaml +1 -3
  27. package/framework/i18n/pt.yaml +1 -3
  28. package/framework/intelligence/context-engine.md +3 -3
  29. package/framework/orchestrator/chati.md +24 -24
  30. package/framework/quality-gates/planning-gate.md +1 -1
  31. package/framework/schemas/context.schema.json +1 -1
  32. package/framework/schemas/session.schema.json +3 -3
  33. package/framework/tasks/architect-api-design.md +1 -1
  34. package/framework/tasks/architect-consolidate.md +1 -1
  35. package/framework/tasks/architect-db-design.md +1 -1
  36. package/framework/tasks/architect-design.md +1 -1
  37. package/framework/tasks/architect-security-review.md +1 -1
  38. package/framework/tasks/architect-stack-selection.md +1 -1
  39. package/framework/tasks/brief-consolidate.md +1 -1
  40. package/framework/tasks/brief-constraint-identify.md +1 -1
  41. package/framework/tasks/brief-extract-requirements.md +1 -1
  42. package/framework/tasks/brief-stakeholder-map.md +1 -1
  43. package/framework/tasks/brief-validate-completeness.md +1 -1
  44. package/framework/tasks/brownfield-wu-architecture-map.md +1 -1
  45. package/framework/tasks/brownfield-wu-deep-discovery.md +1 -1
  46. package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
  47. package/framework/tasks/brownfield-wu-migration-plan.md +1 -1
  48. package/framework/tasks/brownfield-wu-report.md +1 -1
  49. package/framework/tasks/brownfield-wu-risk-assess.md +1 -1
  50. package/framework/tasks/detail-acceptance-criteria.md +1 -1
  51. package/framework/tasks/detail-consolidate.md +1 -1
  52. package/framework/tasks/detail-edge-case-analysis.md +1 -1
  53. package/framework/tasks/detail-expand-prd.md +1 -1
  54. package/framework/tasks/detail-nfr-extraction.md +1 -1
  55. package/framework/tasks/greenfield-wu-analyze-empty.md +1 -1
  56. package/framework/tasks/greenfield-wu-report.md +1 -1
  57. package/framework/tasks/greenfield-wu-scaffold-detection.md +1 -1
  58. package/framework/tasks/greenfield-wu-tech-stack-assess.md +1 -1
  59. package/framework/tasks/orchestrator-deviation.md +3 -3
  60. package/framework/tasks/orchestrator-escalate.md +2 -2
  61. package/framework/tasks/orchestrator-handoff.md +6 -6
  62. package/framework/tasks/orchestrator-health.md +1 -1
  63. package/framework/tasks/orchestrator-mode-switch.md +20 -20
  64. package/framework/tasks/orchestrator-resume.md +6 -6
  65. package/framework/tasks/orchestrator-route.md +7 -7
  66. package/framework/tasks/orchestrator-status.md +8 -8
  67. package/framework/tasks/orchestrator-suggest-mode.md +13 -13
  68. package/framework/tasks/phases-breakdown.md +1 -1
  69. package/framework/tasks/phases-dependency-mapping.md +1 -1
  70. package/framework/tasks/phases-mvp-scoping.md +1 -1
  71. package/framework/tasks/qa-impl-regression-check.md +2 -2
  72. package/framework/tasks/qa-planning-consolidate.md +3 -3
  73. package/framework/tasks/qa-planning-coverage-plan.md +1 -1
  74. package/framework/tasks/qa-planning-gate-define.md +7 -7
  75. package/framework/tasks/qa-planning-risk-matrix.md +1 -1
  76. package/framework/tasks/qa-planning-test-strategy.md +1 -1
  77. package/framework/tasks/tasks-acceptance-write.md +1 -1
  78. package/framework/tasks/tasks-consolidate.md +1 -1
  79. package/framework/tasks/tasks-decompose.md +1 -1
  80. package/framework/tasks/tasks-estimate.md +1 -1
  81. package/framework/tasks/ux-a11y-check.md +1 -1
  82. package/framework/tasks/ux-component-map.md +1 -1
  83. package/framework/tasks/ux-consolidate.md +1 -1
  84. package/framework/tasks/ux-user-flow.md +1 -1
  85. package/framework/tasks/ux-wireframe.md +1 -1
  86. package/framework/workflows/brownfield-discovery.yaml +7 -7
  87. package/framework/workflows/brownfield-fullstack.yaml +9 -9
  88. package/framework/workflows/brownfield-service.yaml +5 -5
  89. package/framework/workflows/brownfield-ui.yaml +5 -5
  90. package/framework/workflows/greenfield-fullstack.yaml +9 -9
  91. package/package.json +3 -3
  92. package/scripts/bundle-framework.js +1 -1
  93. package/scripts/generate-signing-key.js +33 -0
  94. package/scripts/health-check.js +8 -8
  95. package/scripts/populate-entity-registry.js +1 -1
  96. package/scripts/semantic-lint.js +328 -0
  97. package/scripts/sign-manifest.js +53 -0
  98. package/scripts/validate-agents.js +1 -1
  99. package/scripts/validate-package.js +257 -0
  100. package/scripts/validate-tasks.js +1 -1
  101. package/src/autonomy/autonomous-gate.js +2 -2
  102. package/src/autonomy/progress-reporter.js +9 -9
  103. package/src/config/mcp-configs.js +16 -85
  104. package/src/context/engine.js +2 -2
  105. package/src/context/layers/l1-global.js +3 -3
  106. package/src/dashboard/layout.js +7 -7
  107. package/src/dashboard/renderer.js +2 -2
  108. package/src/gates/g1-planning-complete.js +9 -9
  109. package/src/gates/g4-qa-implementation.js +9 -9
  110. package/src/installer/core.js +27 -12
  111. package/src/installer/manifest.js +33 -1
  112. package/src/installer/signing-public-key.pem +3 -0
  113. package/src/installer/templates.js +1 -1
  114. package/src/installer/validator.js +41 -8
  115. package/src/intelligence/registry-manager.js +4 -4
  116. package/src/memory/agent-memory.js +20 -0
  117. package/src/memory/session-digest.js +1 -1
  118. package/src/orchestrator/agent-selector.js +10 -10
  119. package/src/orchestrator/intent-classifier.js +4 -4
  120. package/src/orchestrator/pipeline-manager.js +4 -4
  121. package/src/orchestrator/pipeline-state.js +1 -1
  122. package/src/orchestrator/session-manager.js +3 -3
  123. package/src/tasks/executor.js +1 -1
  124. package/src/tasks/loader.js +1 -1
  125. package/src/terminal/prompt-builder.js +10 -10
  126. package/src/wizard/i18n.js +1 -3
  127. package/src/wizard/index.js +10 -15
  128. package/src/wizard/questions.js +7 -70
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Semantic Linter — Validates cross-reference consistency across the framework.
5
+ *
6
+ * Goes beyond structural validation (validate-agents, validate-tasks) to check
7
+ * semantic consistency: entity registry integrity, domain-agent alignment,
8
+ * workflow references, i18n completeness, schema existence, constitution articles.
9
+ *
10
+ * Exports:
11
+ * semanticLint(frameworkDir) → { errors, warnings, checks, passed }
12
+ */
13
+
14
+ import { existsSync, readFileSync, readdirSync } from 'fs';
15
+ import { join, dirname, basename } from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import yaml from 'js-yaml';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+
21
+ /**
22
+ * Check that all paths in entity-registry.yaml point to existing files.
23
+ */
24
+ function checkEntityRegistryIntegrity(frameworkDir, results) {
25
+ results.checks++;
26
+ const registryPath = join(frameworkDir, 'data', 'entity-registry.yaml');
27
+
28
+ if (!existsSync(registryPath)) {
29
+ results.errors.push('Entity registry not found at data/entity-registry.yaml');
30
+ return;
31
+ }
32
+
33
+ const registry = yaml.load(readFileSync(registryPath, 'utf-8'));
34
+ const entities = registry.entities || {};
35
+ let total = 0;
36
+ let found = 0;
37
+ const missing = [];
38
+
39
+ for (const category of Object.values(entities)) {
40
+ for (const [name, entity] of Object.entries(category)) {
41
+ if (!entity.path) continue;
42
+ total++;
43
+
44
+ // Path in registry is relative to project root (e.g., "chati.dev/agents/...")
45
+ // Strip "chati.dev/" prefix to get path relative to frameworkDir
46
+ const relPath = entity.path.replace(/^chati\.dev\//, '');
47
+ if (existsSync(join(frameworkDir, relPath))) {
48
+ found++;
49
+ } else {
50
+ missing.push(`${name}: ${entity.path}`);
51
+ }
52
+ }
53
+ }
54
+
55
+ if (missing.length === 0) {
56
+ results.passed++;
57
+ results.details.push(`Entity registry integrity: ${found}/${total} paths exist`);
58
+ } else {
59
+ results.errors.push(`Entity registry: ${missing.length} missing files — ${missing.slice(0, 5).join('; ')}${missing.length > 5 ? '...' : ''}`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Check that each agent in domains/agents/ has a corresponding agent definition file.
65
+ */
66
+ function checkDomainAgentAlignment(frameworkDir, results) {
67
+ results.checks++;
68
+ const domainsDir = join(frameworkDir, 'domains', 'agents');
69
+
70
+ if (!existsSync(domainsDir)) {
71
+ results.errors.push('Domain agents directory not found at domains/agents/');
72
+ return;
73
+ }
74
+
75
+ const domainFiles = readdirSync(domainsDir).filter(f => f.endsWith('.yaml'));
76
+ const mismatches = [];
77
+
78
+ for (const file of domainFiles) {
79
+ const agentName = basename(file, '.yaml');
80
+
81
+ // orchestrator has a different path
82
+ if (agentName === 'orchestrator') {
83
+ if (!existsSync(join(frameworkDir, 'orchestrator', 'chati.md'))) {
84
+ mismatches.push('orchestrator');
85
+ }
86
+ continue;
87
+ }
88
+
89
+ // Other agents: check all category dirs
90
+ const agentDirs = ['agents/planning', 'agents/quality', 'agents/build', 'agents/deploy'];
91
+ const found = agentDirs.some(dir =>
92
+ existsSync(join(frameworkDir, dir, `${agentName}.md`))
93
+ );
94
+
95
+ if (!found) {
96
+ mismatches.push(agentName);
97
+ }
98
+ }
99
+
100
+ if (mismatches.length === 0) {
101
+ results.passed++;
102
+ results.details.push(`Domain-agent alignment: ${domainFiles.length}/${domainFiles.length} agents matched`);
103
+ } else {
104
+ results.errors.push(`Domain-agent mismatch: ${mismatches.join(', ')} have domain YAML but no agent .md`);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Check that i18n files have consistent keys across all languages.
110
+ */
111
+ function checkI18nCompleteness(frameworkDir, results) {
112
+ results.checks++;
113
+ const i18nDir = join(frameworkDir, 'i18n');
114
+
115
+ if (!existsSync(i18nDir)) {
116
+ results.errors.push('i18n directory not found');
117
+ return;
118
+ }
119
+
120
+ const languages = ['en', 'pt', 'es', 'fr'];
121
+ const keysByLang = {};
122
+
123
+ for (const lang of languages) {
124
+ const filePath = join(i18nDir, `${lang}.yaml`);
125
+ if (!existsSync(filePath)) {
126
+ results.errors.push(`i18n file missing: ${lang}.yaml`);
127
+ return;
128
+ }
129
+
130
+ const content = yaml.load(readFileSync(filePath, 'utf-8'));
131
+ keysByLang[lang] = flattenKeys(content);
132
+ }
133
+
134
+ // Use 'en' as reference
135
+ const refKeys = new Set(keysByLang.en);
136
+ const missingByLang = {};
137
+
138
+ for (const lang of languages) {
139
+ if (lang === 'en') continue;
140
+ const langKeys = new Set(keysByLang[lang]);
141
+
142
+ for (const key of refKeys) {
143
+ // Skip non-translatable keys
144
+ if (key === 'language' || key === 'name') continue;
145
+
146
+ if (!langKeys.has(key)) {
147
+ if (!missingByLang[lang]) missingByLang[lang] = [];
148
+ missingByLang[lang].push(key);
149
+ }
150
+ }
151
+ }
152
+
153
+ const totalMissing = Object.values(missingByLang).reduce((sum, arr) => sum + arr.length, 0);
154
+
155
+ if (totalMissing === 0) {
156
+ results.passed++;
157
+ results.details.push(`i18n completeness: all ${languages.length} languages have consistent keys`);
158
+ } else {
159
+ for (const [lang, keys] of Object.entries(missingByLang)) {
160
+ results.warnings.push(`i18n: '${lang}' missing ${keys.length} key(s): ${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}`);
161
+ }
162
+ results.passed++; // warnings don't fail the check
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Check that all expected schema files exist.
168
+ */
169
+ function checkSchemaExistence(frameworkDir, results) {
170
+ results.checks++;
171
+ const schemasDir = join(frameworkDir, 'schemas');
172
+ const expectedSchemas = [
173
+ 'session.schema.json', 'config.schema.json', 'task.schema.json',
174
+ 'context.schema.json', 'memory.schema.json',
175
+ ];
176
+
177
+ const missing = expectedSchemas.filter(s => !existsSync(join(schemasDir, s)));
178
+
179
+ if (missing.length === 0) {
180
+ results.passed++;
181
+ results.details.push(`Schema existence: ${expectedSchemas.length}/${expectedSchemas.length} schemas found`);
182
+ } else {
183
+ results.errors.push(`Missing schemas: ${missing.join(', ')}`);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Check constitution has >= 17 articles.
189
+ */
190
+ function checkConstitution(frameworkDir, results) {
191
+ results.checks++;
192
+ const constitutionPath = join(frameworkDir, 'constitution.md');
193
+
194
+ if (!existsSync(constitutionPath)) {
195
+ results.errors.push('constitution.md not found');
196
+ return;
197
+ }
198
+
199
+ const content = readFileSync(constitutionPath, 'utf-8');
200
+ const articleCount = (content.match(/^## Article/gm) || []).length;
201
+
202
+ if (articleCount >= 17) {
203
+ results.passed++;
204
+ results.details.push(`Constitution: ${articleCount} articles found`);
205
+ } else {
206
+ results.errors.push(`Constitution has only ${articleCount} articles (expected >= 17)`);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Check that workflow files reference valid agents.
212
+ */
213
+ function checkWorkflowAgentRefs(frameworkDir, results) {
214
+ results.checks++;
215
+ const workflowsDir = join(frameworkDir, 'workflows');
216
+
217
+ if (!existsSync(workflowsDir)) {
218
+ results.errors.push('workflows/ directory not found');
219
+ return;
220
+ }
221
+
222
+ // Collect known agent names
223
+ const knownAgents = new Set(['orchestrator']);
224
+ const agentDirs = ['agents/planning', 'agents/quality', 'agents/build', 'agents/deploy'];
225
+ for (const dir of agentDirs) {
226
+ const fullDir = join(frameworkDir, dir);
227
+ if (!existsSync(fullDir)) continue;
228
+ for (const file of readdirSync(fullDir)) {
229
+ if (file.endsWith('.md')) {
230
+ knownAgents.add(basename(file, '.md'));
231
+ }
232
+ }
233
+ }
234
+
235
+ const workflowFiles = readdirSync(workflowsDir).filter(f => f.endsWith('.yaml'));
236
+ const unknownRefs = [];
237
+
238
+ for (const file of workflowFiles) {
239
+ const content = readFileSync(join(workflowsDir, file), 'utf-8');
240
+ // Look for agent references in YAML (agent: xxx or - xxx in steps)
241
+ const agentRefs = content.match(/agent:\s*(\S+)/g) || [];
242
+ for (const ref of agentRefs) {
243
+ const agentName = ref.replace('agent:', '').trim();
244
+ if (agentName && !knownAgents.has(agentName)) {
245
+ unknownRefs.push(`${file}: ${agentName}`);
246
+ }
247
+ }
248
+ }
249
+
250
+ if (unknownRefs.length === 0) {
251
+ results.passed++;
252
+ results.details.push(`Workflow-agent references: ${workflowFiles.length} workflows, all agents valid`);
253
+ } else {
254
+ results.warnings.push(`Unknown agent refs in workflows: ${unknownRefs.join('; ')}`);
255
+ results.passed++; // warnings don't fail
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Flatten a nested object into dot-notation keys.
261
+ */
262
+ function flattenKeys(obj, prefix = '') {
263
+ const keys = [];
264
+ for (const [key, value] of Object.entries(obj)) {
265
+ const fullKey = prefix ? `${prefix}.${key}` : key;
266
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
267
+ keys.push(...flattenKeys(value, fullKey));
268
+ } else {
269
+ keys.push(fullKey);
270
+ }
271
+ }
272
+ return keys;
273
+ }
274
+
275
+ /**
276
+ * Run all semantic lint checks.
277
+ * @param {string} frameworkDir - Path to the framework directory (chati.dev/)
278
+ * @returns {{ errors: string[], warnings: string[], details: string[], checks: number, passed: number }}
279
+ */
280
+ export function semanticLint(frameworkDir) {
281
+ const results = { errors: [], warnings: [], details: [], checks: 0, passed: 0 };
282
+
283
+ checkEntityRegistryIntegrity(frameworkDir, results);
284
+ checkDomainAgentAlignment(frameworkDir, results);
285
+ checkWorkflowAgentRefs(frameworkDir, results);
286
+ checkI18nCompleteness(frameworkDir, results);
287
+ checkSchemaExistence(frameworkDir, results);
288
+ checkConstitution(frameworkDir, results);
289
+
290
+ return results;
291
+ }
292
+
293
+ /**
294
+ * Format results for CLI output.
295
+ */
296
+ function formatResults(results) {
297
+ const status = results.errors.length === 0 ? 'PASS' : 'FAIL';
298
+ const lines = [
299
+ `Semantic Lint: ${status} (${results.passed}/${results.checks} checks, ${results.warnings.length} warnings, ${results.errors.length} errors)`,
300
+ '',
301
+ ];
302
+
303
+ for (const detail of results.details) {
304
+ lines.push(` ✓ ${detail}`);
305
+ }
306
+
307
+ for (const warning of results.warnings) {
308
+ lines.push(` ⚠ ${warning}`);
309
+ }
310
+
311
+ for (const error of results.errors) {
312
+ lines.push(` ✗ ${error}`);
313
+ }
314
+
315
+ return lines.join('\n');
316
+ }
317
+
318
+ // CLI entry point
319
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
320
+ const frameworkDir = join(__dirname, '..', '..', '..', 'chati.dev');
321
+ const results = semanticLint(frameworkDir);
322
+
323
+ console.log(formatResults(results));
324
+
325
+ if (results.errors.length > 0) {
326
+ process.exit(1);
327
+ }
328
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Sign the framework manifest with Ed25519.
3
+ * Generates manifest.json (file hashes) + manifest.sig (signature).
4
+ *
5
+ * Run: node scripts/sign-manifest.js
6
+ * Requires: .signing-key.pem (generated by generate-signing-key.js)
7
+ */
8
+ import { sign, createPrivateKey } from 'crypto';
9
+ import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs';
10
+ import { join, dirname, relative } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { generateManifest } from '../src/installer/manifest.js';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const frameworkDir = join(__dirname, '..', 'framework');
16
+ const privateKeyPath = join(__dirname, '..', '.signing-key.pem');
17
+
18
+ if (!existsSync(frameworkDir)) {
19
+ console.error('Framework directory not found. Run bundle-framework.js first.');
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!existsSync(privateKeyPath)) {
24
+ console.warn('No signing key found at .signing-key.pem — skipping manifest signing.');
25
+ process.exit(0);
26
+ }
27
+
28
+ function collectFiles(dir, base = dir) {
29
+ let files = [];
30
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
31
+ const full = join(dir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ files = files.concat(collectFiles(full, base));
34
+ } else if (entry.name !== 'manifest.json' && entry.name !== 'manifest.sig') {
35
+ files.push(relative(base, full));
36
+ }
37
+ }
38
+ return files.sort();
39
+ }
40
+
41
+ const files = collectFiles(frameworkDir);
42
+ const version = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
43
+ const manifest = generateManifest(frameworkDir, files, version);
44
+
45
+ const manifestJson = JSON.stringify(manifest, Object.keys(manifest).sort(), 2);
46
+
47
+ const privateKey = createPrivateKey(readFileSync(privateKeyPath));
48
+ const signature = sign(null, Buffer.from(manifestJson), privateKey);
49
+
50
+ writeFileSync(join(frameworkDir, 'manifest.json'), manifestJson + '\n');
51
+ writeFileSync(join(frameworkDir, 'manifest.sig'), signature.toString('base64'));
52
+
53
+ console.log(`Signed manifest: ${Object.keys(manifest.files).length} files, v${version}`);
@@ -19,7 +19,7 @@ import yaml from 'js-yaml';
19
19
  * Expected agents organized by category.
20
20
  */
21
21
  export const EXPECTED_AGENTS = {
22
- clarity: ['greenfield-wu', 'brownfield-wu', 'brief', 'detail', 'architect', 'ux', 'phases', 'tasks'],
22
+ planning: ['greenfield-wu', 'brownfield-wu', 'brief', 'detail', 'architect', 'ux', 'phases', 'tasks'],
23
23
  quality: ['qa-planning', 'qa-implementation'],
24
24
  build: ['dev'],
25
25
  deploy: ['devops'],
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Package Completeness Validator
5
+ *
6
+ * Validates that the npm package has all required files before publishing.
7
+ * Run as part of prepublishOnly (after bundle, before sign-manifest).
8
+ *
9
+ * Exports:
10
+ * validatePackage(packageRoot) → { errors, warnings, checks, passed }
11
+ */
12
+
13
+ import { existsSync, readFileSync, readdirSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+
19
+ /**
20
+ * Expected directories in framework/ (must match bundle-framework.js)
21
+ */
22
+ const EXPECTED_BUNDLE_DIRS = [
23
+ 'orchestrator',
24
+ 'agents/planning', 'agents/quality', 'agents/build', 'agents/deploy',
25
+ 'templates', 'workflows', 'quality-gates',
26
+ 'schemas', 'frameworks', 'intelligence', 'patterns',
27
+ 'hooks', 'domains',
28
+ 'i18n', 'migrations', 'data',
29
+ 'tasks', 'context',
30
+ ];
31
+
32
+ const EXPECTED_ROOT_FILES = ['constitution.md', 'config.yaml'];
33
+
34
+ const SENSITIVE_FILES = ['.signing-key.pem', '.env', '.env.local', '.env.production'];
35
+
36
+ /**
37
+ * Validate that framework/ directory exists.
38
+ */
39
+ function checkFrameworkExists(packageRoot, results) {
40
+ results.checks++;
41
+ const frameworkDir = join(packageRoot, 'framework');
42
+ if (existsSync(frameworkDir)) {
43
+ results.passed++;
44
+ } else {
45
+ results.errors.push('framework/ directory not found. Run `npm run bundle` first.');
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Validate all expected bundle directories exist.
51
+ */
52
+ function checkBundledDirs(packageRoot, results) {
53
+ results.checks++;
54
+ const frameworkDir = join(packageRoot, 'framework');
55
+ if (!existsSync(frameworkDir)) return;
56
+
57
+ const missing = [];
58
+ for (const dir of EXPECTED_BUNDLE_DIRS) {
59
+ if (!existsSync(join(frameworkDir, dir))) {
60
+ missing.push(dir);
61
+ }
62
+ }
63
+
64
+ if (missing.length === 0) {
65
+ results.passed++;
66
+ } else {
67
+ results.errors.push(`Missing framework directories: ${missing.join(', ')}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Validate root framework files exist.
73
+ */
74
+ function checkRootFiles(packageRoot, results) {
75
+ results.checks++;
76
+ const frameworkDir = join(packageRoot, 'framework');
77
+ if (!existsSync(frameworkDir)) return;
78
+
79
+ const missing = [];
80
+ for (const file of EXPECTED_ROOT_FILES) {
81
+ if (!existsSync(join(frameworkDir, file))) {
82
+ missing.push(file);
83
+ }
84
+ }
85
+
86
+ if (missing.length === 0) {
87
+ results.passed++;
88
+ } else {
89
+ results.errors.push(`Missing framework root files: ${missing.join(', ')}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Check that no sensitive files leaked into the bundle.
95
+ */
96
+ function checkNoSensitiveFiles(packageRoot, results) {
97
+ results.checks++;
98
+ const found = [];
99
+
100
+ // Only check inside publishable directories (bin/, src/, assets/, framework/, scripts/)
101
+ // Root-level files like .signing-key.pem are excluded by package.json "files" field
102
+ const publishableDirs = ['framework', 'src', 'scripts', 'bin', 'assets'];
103
+ for (const file of SENSITIVE_FILES) {
104
+ for (const dir of publishableDirs) {
105
+ if (existsSync(join(packageRoot, dir, file))) {
106
+ found.push(`${dir}/${file}`);
107
+ }
108
+ }
109
+ }
110
+
111
+ if (found.length === 0) {
112
+ results.passed++;
113
+ } else {
114
+ results.errors.push(`Sensitive files found in package: ${found.join(', ')}`);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Validate that all package.json exports resolve to existing files.
120
+ */
121
+ function checkExportsResolve(packageRoot, results) {
122
+ results.checks++;
123
+ const pkgPath = join(packageRoot, 'package.json');
124
+ if (!existsSync(pkgPath)) return;
125
+
126
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
127
+ const exports = pkg.exports || {};
128
+ const missing = [];
129
+
130
+ for (const [key, value] of Object.entries(exports)) {
131
+ const filePath = join(packageRoot, value);
132
+ if (!existsSync(filePath)) {
133
+ missing.push(`${key} → ${value}`);
134
+ }
135
+ }
136
+
137
+ if (missing.length === 0) {
138
+ results.passed++;
139
+ } else {
140
+ results.errors.push(`Unresolved exports: ${missing.join(', ')}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Validate that bin entry exists.
146
+ */
147
+ function checkBinExists(packageRoot, results) {
148
+ results.checks++;
149
+ const pkgPath = join(packageRoot, 'package.json');
150
+ if (!existsSync(pkgPath)) return;
151
+
152
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
153
+ const bin = pkg.bin || {};
154
+ const missing = [];
155
+
156
+ for (const [name, path] of Object.entries(bin)) {
157
+ if (!existsSync(join(packageRoot, path))) {
158
+ missing.push(`${name} → ${path}`);
159
+ }
160
+ }
161
+
162
+ if (missing.length === 0) {
163
+ results.passed++;
164
+ } else {
165
+ results.errors.push(`Missing bin entries: ${missing.join(', ')}`);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Compare entity count in registry vs actual files in framework/.
171
+ */
172
+ function checkEntityCount(packageRoot, results) {
173
+ results.checks++;
174
+ const registryPath = join(packageRoot, 'framework', 'data', 'entity-registry.yaml');
175
+ if (!existsSync(registryPath)) {
176
+ results.warnings.push('Entity registry not found in framework/data/ — skipping count check.');
177
+ results.passed++;
178
+ return;
179
+ }
180
+
181
+ // Count files in framework/ recursively (excluding manifest.json, manifest.sig)
182
+ let fileCount = 0;
183
+ function countFiles(dir) {
184
+ try {
185
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
186
+ if (entry.isDirectory()) {
187
+ countFiles(join(dir, entry.name));
188
+ } else if (entry.name !== 'manifest.json' && entry.name !== 'manifest.sig') {
189
+ fileCount++;
190
+ }
191
+ }
192
+ } catch { /* skip unreadable dirs */ }
193
+ }
194
+
195
+ countFiles(join(packageRoot, 'framework'));
196
+
197
+ if (fileCount > 0) {
198
+ results.passed++;
199
+ } else {
200
+ results.errors.push('Framework directory is empty (0 files).');
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Validate the npm package completeness.
206
+ * @param {string} packageRoot - Root of the package (packages/chati-dev/)
207
+ * @returns {{ errors: string[], warnings: string[], checks: number, passed: number }}
208
+ */
209
+ export function validatePackage(packageRoot) {
210
+ const results = { errors: [], warnings: [], checks: 0, passed: 0 };
211
+
212
+ checkFrameworkExists(packageRoot, results);
213
+ checkBundledDirs(packageRoot, results);
214
+ checkRootFiles(packageRoot, results);
215
+ checkNoSensitiveFiles(packageRoot, results);
216
+ checkExportsResolve(packageRoot, results);
217
+ checkBinExists(packageRoot, results);
218
+ checkEntityCount(packageRoot, results);
219
+
220
+ return results;
221
+ }
222
+
223
+ /**
224
+ * Format results for CLI output.
225
+ */
226
+ function formatResults(results) {
227
+ const status = results.errors.length === 0 ? 'PASS' : 'FAIL';
228
+ const lines = [
229
+ `Package Validation: ${status} (${results.passed}/${results.checks} checks passed)`,
230
+ ];
231
+
232
+ if (results.errors.length > 0) {
233
+ lines.push('');
234
+ lines.push('Errors:');
235
+ results.errors.forEach(e => lines.push(` ✗ ${e}`));
236
+ }
237
+
238
+ if (results.warnings.length > 0) {
239
+ lines.push('');
240
+ lines.push('Warnings:');
241
+ results.warnings.forEach(w => lines.push(` ⚠ ${w}`));
242
+ }
243
+
244
+ return lines.join('\n');
245
+ }
246
+
247
+ // CLI entry point
248
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
249
+ const packageRoot = join(__dirname, '..');
250
+ const results = validatePackage(packageRoot);
251
+
252
+ console.log(formatResults(results));
253
+
254
+ if (results.errors.length > 0) {
255
+ process.exit(1);
256
+ }
257
+ }
@@ -24,7 +24,7 @@ const REQUIRED_FIELDS = ['id', 'agent'];
24
24
  /**
25
25
  * Valid phase values.
26
26
  */
27
- const VALID_PHASES = ['clarity', 'build', 'deploy', 'validate', 'quality'];
27
+ const VALID_PHASES = ['planning', 'build', 'deploy', 'validate', 'quality'];
28
28
 
29
29
  // ---------------------------------------------------------------------------
30
30
  // Internal Helpers
@@ -16,8 +16,8 @@ export const GATE_RESULTS = {
16
16
 
17
17
  // Minimum passing scores per agent (conservative: 90 default)
18
18
  const GATE_THRESHOLDS = {
19
- 'qa-planning': 95, // Highest — gates clarity→build transition
20
- 'qa-implementation': 90, // Gates build→deploy transition
19
+ 'qa-planning': 95, // Highest — gates planning→build transition
20
+ 'qa-implementation': 95, // Gates build→deploy transition
21
21
  'brief': 90, // Requirements are critical
22
22
  'detail': 90,
23
23
  'architect': 90,