chati-dev 3.2.5 → 3.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.
Files changed (98) hide show
  1. package/LICENSE +96 -0
  2. package/bin/chati.js +46 -0
  3. package/framework/agents/build/dev.md +122 -1
  4. package/framework/agents/deploy/devops.md +128 -3
  5. package/framework/agents/discover/brief.md +77 -15
  6. package/framework/agents/discover/brownfield-wu.md +2 -2
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +3 -3
  10. package/framework/agents/plan/phases.md +127 -2
  11. package/framework/agents/plan/tasks.md +127 -2
  12. package/framework/agents/plan/ux.md +269 -22
  13. package/framework/agents/quality/qa-implementation.md +172 -8
  14. package/framework/agents/quality/qa-planning.md +147 -2
  15. package/framework/config.yaml +9 -5
  16. package/framework/constitution.md +7 -1
  17. package/framework/context/quality.md +1 -1
  18. package/framework/context/root.md +1 -1
  19. package/framework/hooks/constitution-guard.js +18 -2
  20. package/framework/hooks/mode-governance.js +3 -3
  21. package/framework/hooks/read-protection.js +10 -2
  22. package/framework/i18n/en.yaml +6 -0
  23. package/framework/i18n/es.yaml +6 -0
  24. package/framework/i18n/fr.yaml +6 -0
  25. package/framework/i18n/pt.yaml +6 -0
  26. package/framework/orchestrator/chati.md +102 -6
  27. package/framework/schemas/task.schema.json +1 -1
  28. package/framework/tasks/architect-dep-audit.md +128 -0
  29. package/framework/tasks/architect-stack-selection.md +28 -0
  30. package/framework/workflows/brownfield-fullstack.yaml +2 -2
  31. package/framework/workflows/brownfield-service.yaml +2 -2
  32. package/framework/workflows/brownfield-ui.yaml +2 -2
  33. package/framework/workflows/greenfield-fullstack.yaml +6 -2
  34. package/framework/workflows/quick-flow.yaml +7 -5
  35. package/framework/workflows/standard-flow.yaml +171 -0
  36. package/package.json +4 -2
  37. package/src/api/index.js +129 -0
  38. package/src/autonomy/build-loop.js +93 -6
  39. package/src/autonomy/build-state.js +20 -2
  40. package/src/autonomy/cause-analyzer.js +177 -0
  41. package/src/autonomy/escalation.js +214 -0
  42. package/src/autonomy/safety-net.js +23 -5
  43. package/src/autonomy/worktree-manager.js +245 -0
  44. package/src/config/agent-customizer.js +227 -0
  45. package/src/config/ide-configs.js +57 -27
  46. package/src/decision/analyzer.js +148 -0
  47. package/src/decision/registry-healer.js +38 -21
  48. package/src/extensions/loader.js +151 -0
  49. package/src/extensions/registry.js +134 -0
  50. package/src/gates/circuit-breaker.js +32 -0
  51. package/src/gates/g3-implementation.js +30 -4
  52. package/src/gates/g4-qa-implementation.js +34 -5
  53. package/src/gates/gate-base.js +9 -0
  54. package/src/health/auto-fix.js +216 -0
  55. package/src/installer/core.js +24 -11
  56. package/src/installer/provider-overlay.js +82 -0
  57. package/src/installer/templates.js +22 -10
  58. package/src/installer/transaction.js +3 -2
  59. package/src/installer/validator.js +74 -0
  60. package/src/intelligence/context-status.js +9 -5
  61. package/src/intelligence/document-sharder.js +221 -0
  62. package/src/intelligence/elicitation.js +265 -0
  63. package/src/intelligence/timeline.js +5 -0
  64. package/src/memory/gotchas.js +78 -2
  65. package/src/merger/semantic-merger.js +292 -0
  66. package/src/orchestrator/agent-selector.js +20 -0
  67. package/src/orchestrator/handoff-engine.js +77 -0
  68. package/src/orchestrator/index.js +0 -8
  69. package/src/orchestrator/intent-classifier.js +182 -0
  70. package/src/orchestrator/pipeline-manager.js +125 -1
  71. package/src/orchestrator/session-manager.js +164 -2
  72. package/src/quality/metrics-collector.js +283 -0
  73. package/src/quality/test-runner.js +368 -0
  74. package/src/telemetry/collector.js +83 -0
  75. package/src/telemetry/config.js +119 -0
  76. package/src/telemetry/index.js +11 -0
  77. package/src/telemetry/schema.js +104 -0
  78. package/src/telemetry/sender.js +60 -0
  79. package/src/terminal/cli-registry.js +7 -1
  80. package/src/terminal/cost-tracker.js +197 -0
  81. package/src/terminal/handoff-parser.js +61 -4
  82. package/src/terminal/prompt-builder.js +56 -18
  83. package/src/terminal/rate-limiter.js +172 -0
  84. package/src/terminal/run-agent.js +39 -0
  85. package/src/terminal/run-parallel.js +22 -1
  86. package/src/terminal/spawner.js +181 -3
  87. package/src/upgrade/migrator.js +2 -2
  88. package/src/utils/event-bus.js +126 -0
  89. package/src/utils/file-lock.js +291 -0
  90. package/src/utils/schema-validator.js +226 -0
  91. package/src/wizard/i18n.js +11 -0
  92. package/src/wizard/index.js +42 -20
  93. package/src/wizard/questions.js +200 -39
  94. package/src/autonomy/execution-profile.js +0 -151
  95. package/src/intelligence/file-tracker.js +0 -117
  96. package/src/memory/gotchas-auto-capture.js +0 -253
  97. package/src/orchestrator/pipeline-state.js +0 -223
  98. package/src/terminal/wave-analyzer.js +0 -143
@@ -12,7 +12,7 @@
12
12
  * WAIVED — Human explicitly overrode
13
13
  */
14
14
 
15
- import { existsSync } from 'node:fs';
15
+ import { existsSync, readFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { GateBase, determineVerdict } from './gate-base.js';
18
18
  import { loadSession } from '../orchestrator/session-manager.js';
@@ -69,26 +69,55 @@ export class QAImplementationGate extends GateBase {
69
69
  evidence.noCriticalBugs = evidence.qaImplHandoff.blockers.length === 0;
70
70
  }
71
71
 
72
- // Check for performance benchmark results
72
+ // Check for performance benchmark results — parse content for real findings
73
73
  const perfPaths = [
74
74
  join(projectDir, 'chati.dev', 'artifacts', 'performance-report.md'),
75
75
  join(projectDir, '.performance-benchmark'),
76
76
  ];
77
77
  for (const perfPath of perfPaths) {
78
78
  if (existsSync(perfPath)) {
79
- evidence.performanceBenchmarksMet = true;
79
+ try {
80
+ const content = readFileSync(perfPath, 'utf-8');
81
+ const hasFailures = /fail|exceeded|timeout|regression|degradation|below\s*threshold/i.test(content);
82
+ const hasPassIndicator = /pass|met|within\s*budget|no\s*(regressions|issues)|acceptable/i.test(content);
83
+ if (hasFailures && !hasPassIndicator) {
84
+ evidence.performanceBenchmarksMet = false;
85
+ } else if (hasPassIndicator) {
86
+ evidence.performanceBenchmarksMet = true;
87
+ } else {
88
+ evidence.performanceBenchmarksMet = null; // Ambiguous
89
+ }
90
+ } catch {
91
+ evidence.performanceBenchmarksMet = null;
92
+ }
80
93
  break;
81
94
  }
82
95
  }
83
96
 
84
- // Check for security scan
97
+ // Check for security scan — parse content for real findings
85
98
  const secPaths = [
86
99
  join(projectDir, 'chati.dev', 'artifacts', 'security-report.md'),
87
100
  join(projectDir, '.security-scan'),
88
101
  ];
89
102
  for (const secPath of secPaths) {
90
103
  if (existsSync(secPath)) {
91
- evidence.securityScanClean = true;
104
+ try {
105
+ const content = readFileSync(secPath, 'utf-8').toLowerCase();
106
+ // Look for indicators of security failures
107
+ const hasBlockers = /blocker|critical|high\s*severity|vulnerability found|failed/i.test(content);
108
+ const hasPassIndicator = /pass|clean|no\s*(issues|vulnerabilities|findings)|score:\s*(9[5-9]|100)/i.test(content);
109
+ if (hasBlockers) {
110
+ evidence.securityScanClean = false;
111
+ } else if (hasPassIndicator) {
112
+ evidence.securityScanClean = true;
113
+ } else {
114
+ // File exists but has no clear pass/fail indicators — ambiguous — not clean by default
115
+ evidence.securityScanClean = null;
116
+ }
117
+ } catch {
118
+ // File exists but cannot be read — treat as not available
119
+ evidence.securityScanClean = null;
120
+ }
92
121
  break;
93
122
  }
94
123
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { evaluateGate, getGateThreshold, resolveGateAction } from '../autonomy/autonomous-gate.js';
10
+ import { track as telemetryTrack } from '../telemetry/collector.js';
10
11
 
11
12
  /**
12
13
  * Gate verdict constants (v3.0.0).
@@ -101,6 +102,14 @@ export class GateBase {
101
102
  const hasCriticalBlocker = warnings.some(w => w.toLowerCase().includes('critical'));
102
103
  const verdict = determineVerdict(gateResult.score, threshold, hasCriticalBlocker);
103
104
 
105
+ // Track telemetry
106
+ telemetryTrack('gate_evaluated', {
107
+ gate: this.id,
108
+ result: verdict,
109
+ score: gateResult.score,
110
+ blockers: warnings.filter(w => w.toLowerCase().includes('critical')),
111
+ });
112
+
104
113
  return {
105
114
  gateId: this.id,
106
115
  gateName: this.name,
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @fileoverview Auto-fix for common health check issues.
3
+ *
4
+ * Automatically repairs stale locks, oversized error logs,
5
+ * missing directories, and other recoverable problems.
6
+ *
7
+ * Constitution Article XIV — Framework Registry Governance.
8
+ */
9
+
10
+ import { existsSync, mkdirSync, rmSync, statSync, readdirSync, truncateSync } from 'fs';
11
+ import { join } from 'path';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Constants
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Maximum error log size before truncation (1 MB). */
18
+ const MAX_ERROR_LOG_SIZE = 1_024 * 1_024;
19
+
20
+ /** Stale lock file age threshold (30 seconds). */
21
+ const STALE_LOCK_AGE_MS = 30_000;
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Public API
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * @typedef {object} FixAction
29
+ * @property {string} action - Description of what was fixed
30
+ * @property {boolean} success - Whether the fix was applied
31
+ * @property {string} [error] - Error message if fix failed
32
+ */
33
+
34
+ /**
35
+ * @typedef {object} AutoFixResult
36
+ * @property {number} fixed - Number of issues fixed
37
+ * @property {number} failed - Number of fixes that failed
38
+ * @property {FixAction[]} actions - Detailed action log
39
+ */
40
+
41
+ /**
42
+ * Run auto-fix on a project directory.
43
+ *
44
+ * @param {string} projectDir
45
+ * @param {object[]} [checkResults=[]] - Results from health checks (for targeted fixes)
46
+ * @returns {AutoFixResult}
47
+ */
48
+ export function autoFix(projectDir, checkResults = []) {
49
+ const actions = [];
50
+
51
+ // Fix 1: Ensure required directories exist
52
+ actions.push(...fixMissingDirectories(projectDir));
53
+
54
+ // Fix 2: Remove stale .lock files
55
+ actions.push(...fixStaleLocks(projectDir));
56
+
57
+ // Fix 3: Truncate oversized error logs
58
+ actions.push(...fixOversizedLogs(projectDir));
59
+
60
+ // Fix 4: Clean up orphaned worktrees directory entries
61
+ actions.push(...fixOrphanedWorktrees(projectDir));
62
+
63
+ const fixed = actions.filter(a => a.success).length;
64
+ const failed = actions.filter(a => !a.success).length;
65
+
66
+ return { fixed, failed, actions };
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Individual Fixers
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Ensure required .chati/ subdirectories exist.
75
+ *
76
+ * @param {string} projectDir
77
+ * @returns {FixAction[]}
78
+ */
79
+ function fixMissingDirectories(projectDir) {
80
+ const actions = [];
81
+ const requiredDirs = [
82
+ '.chati',
83
+ '.chati/memory',
84
+ '.chati/metrics',
85
+ ];
86
+
87
+ for (const dir of requiredDirs) {
88
+ const fullPath = join(projectDir, dir);
89
+ if (!existsSync(fullPath)) {
90
+ try {
91
+ mkdirSync(fullPath, { recursive: true });
92
+ actions.push({ action: `Created missing directory: ${dir}`, success: true });
93
+ } catch (err) {
94
+ actions.push({ action: `Failed to create ${dir}`, success: false, error: err.message });
95
+ }
96
+ }
97
+ }
98
+
99
+ return actions;
100
+ }
101
+
102
+ /**
103
+ * Remove .lock files that are stale (old + no active PID).
104
+ *
105
+ * @param {string} projectDir
106
+ * @returns {FixAction[]}
107
+ */
108
+ function fixStaleLocks(projectDir) {
109
+ const actions = [];
110
+ const chatiDir = join(projectDir, '.chati');
111
+
112
+ if (!existsSync(chatiDir)) return actions;
113
+
114
+ try {
115
+ const entries = readdirSync(chatiDir);
116
+ const lockFiles = entries.filter(e => e.endsWith('.lock'));
117
+
118
+ for (const lockFile of lockFiles) {
119
+ const lockPath = join(chatiDir, lockFile);
120
+ try {
121
+ const stat = statSync(lockPath);
122
+ const age = Date.now() - stat.mtimeMs;
123
+
124
+ if (age > STALE_LOCK_AGE_MS) {
125
+ rmSync(lockPath, { force: true });
126
+ actions.push({ action: `Removed stale lock: ${lockFile}`, success: true });
127
+ }
128
+ } catch (err) {
129
+ actions.push({ action: `Failed to check lock ${lockFile}`, success: false, error: err.message });
130
+ }
131
+ }
132
+ } catch {
133
+ // Directory read failed — skip
134
+ }
135
+
136
+ return actions;
137
+ }
138
+
139
+ /**
140
+ * Truncate oversized error/gotcha logs.
141
+ *
142
+ * @param {string} projectDir
143
+ * @returns {FixAction[]}
144
+ */
145
+ function fixOversizedLogs(projectDir) {
146
+ const actions = [];
147
+ const logFiles = [
148
+ join(projectDir, '.chati', 'memory', 'errors.json'),
149
+ join(projectDir, '.chati', 'memory', 'gotchas.json'),
150
+ ];
151
+
152
+ for (const logPath of logFiles) {
153
+ if (!existsSync(logPath)) continue;
154
+
155
+ try {
156
+ const stat = statSync(logPath);
157
+ if (stat.size > MAX_ERROR_LOG_SIZE) {
158
+ truncateSync(logPath, 0);
159
+ actions.push({
160
+ action: `Truncated oversized log: ${logPath.split('/').pop()} (${Math.round(stat.size / 1024)}KB)`,
161
+ success: true,
162
+ });
163
+ }
164
+ } catch (err) {
165
+ actions.push({
166
+ action: `Failed to truncate ${logPath.split('/').pop()}`,
167
+ success: false,
168
+ error: err.message,
169
+ });
170
+ }
171
+ }
172
+
173
+ return actions;
174
+ }
175
+
176
+ /**
177
+ * Clean up orphaned worktree directory entries (empty dirs).
178
+ *
179
+ * @param {string} projectDir
180
+ * @returns {FixAction[]}
181
+ */
182
+ function fixOrphanedWorktrees(projectDir) {
183
+ const actions = [];
184
+ const worktreeDir = join(projectDir, '.chati', 'worktrees');
185
+
186
+ if (!existsSync(worktreeDir)) return actions;
187
+
188
+ try {
189
+ const entries = readdirSync(worktreeDir);
190
+
191
+ for (const entry of entries) {
192
+ const entryPath = join(worktreeDir, entry);
193
+ try {
194
+ const stat = statSync(entryPath);
195
+ if (stat.isDirectory()) {
196
+ const contents = readdirSync(entryPath);
197
+ if (contents.length === 0) {
198
+ rmSync(entryPath, { recursive: true, force: true });
199
+ actions.push({ action: `Removed empty worktree dir: ${entry}`, success: true });
200
+ }
201
+ }
202
+ } catch {
203
+ // Skip entries we can't read
204
+ }
205
+ }
206
+ } catch {
207
+ // Directory read failed
208
+ }
209
+
210
+ return actions;
211
+ }
212
+
213
+ /**
214
+ * Exported constants for testing.
215
+ */
216
+ export { MAX_ERROR_LOG_SIZE, STALE_LOCK_AGE_MS };
@@ -1,11 +1,12 @@
1
1
  import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { IDE_CONFIGS } from '../config/ide-configs.js';
4
+ import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
5
5
  import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
6
6
  import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
7
7
  import { generateContextFiles } from '../config/context-file-generator.js';
8
8
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
9
+ import { generateProviderOverlays } from './provider-overlay.js';
9
10
  import { verifyManifest } from './manifest.js';
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -20,7 +21,7 @@ const FRAMEWORK_SOURCE = existsSync(BUNDLED_SOURCE) ? BUNDLED_SOURCE : MONOREPO_
20
21
  * Install Chati.dev framework into target directory
21
22
  */
22
23
  export async function installFramework(config) {
23
- const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, version, llmProvider } = config;
24
+ const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, version, llmProvider, allProviders } = config;
24
25
 
25
26
  // 0. Verify framework signature (supply chain protection)
26
27
  const manifestPath = join(FRAMEWORK_SOURCE, 'manifest.json');
@@ -40,7 +41,7 @@ export async function installFramework(config) {
40
41
  createDir(join(targetDir, '.chati'));
41
42
  writeFileSync(
42
43
  join(targetDir, '.chati', 'session.yaml'),
43
- generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider }),
44
+ generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders }),
44
45
  'utf-8'
45
46
  );
46
47
 
@@ -58,7 +59,7 @@ export async function installFramework(config) {
58
59
  'i18n', 'migrations', 'data', 'context',
59
60
  'artifacts/0-WU', 'artifacts/1-Brief', 'artifacts/2-PRD',
60
61
  'artifacts/3-Architecture', 'artifacts/4-UX', 'artifacts/5-Phases',
61
- 'artifacts/6-Tasks', 'artifacts/7-QA-Planning', 'artifacts/8-Validation',
62
+ 'artifacts/6-Tasks', 'artifacts/7-QA-Planning', 'artifacts/9-QA-Implementation', 'artifacts/10-Deploy',
62
63
  'artifacts/handoffs', 'artifacts/decisions',
63
64
  ];
64
65
 
@@ -90,16 +91,27 @@ export async function installFramework(config) {
90
91
  // Copy framework files from source (adapted for non-Claude providers)
91
92
  copyFrameworkFiles(frameworkDir, llmProvider || 'claude');
92
93
 
94
+ // Generate provider overlays for secondary CLI providers
95
+ if (allProviders && allProviders.length > 1) {
96
+ generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, llmProvider || 'claude', allProviders);
97
+ }
98
+
93
99
  // Write config.yaml
94
100
  writeFileSync(
95
101
  join(frameworkDir, 'config.yaml'),
96
- generateConfigYaml({ version, projectType, language, selectedIDEs, llmProvider }),
102
+ generateConfigYaml({ version, projectType, language, selectedIDEs, llmProvider, allProviders }),
97
103
  'utf-8'
98
104
  );
99
105
 
100
- // 3. Configure IDEs
106
+ // 3. Configure IDEs (secondary providers get overlay orchestrator path)
107
+ const primary = llmProvider || 'claude';
101
108
  for (const ideKey of selectedIDEs) {
102
- await configureIDE(targetDir, ideKey, selectedMCPs);
109
+ const ideProvider = IDE_TO_PROVIDER[ideKey];
110
+ const isSecondary = ideProvider && ideProvider !== primary && allProviders && allProviders.length > 1;
111
+ const orchestratorPath = isSecondary
112
+ ? `chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
113
+ : 'chati.dev/orchestrator/chati.md';
114
+ await configureIDE(targetDir, ideKey, selectedMCPs, { orchestratorPath, isSecondary, providerName: ideProvider });
103
115
  }
104
116
 
105
117
  const hasClaude = selectedIDEs.includes('claude-code');
@@ -261,7 +273,8 @@ function copyFrameworkFiles(destDir, provider = 'claude') {
261
273
  /**
262
274
  * Configure a specific IDE
263
275
  */
264
- async function configureIDE(targetDir, ideKey, selectedMCPs) {
276
+ async function configureIDE(targetDir, ideKey, selectedMCPs, options = {}) {
277
+ const { orchestratorPath, isSecondary } = options;
265
278
  const config = IDE_CONFIGS[ideKey];
266
279
  if (!config) return;
267
280
 
@@ -290,7 +303,7 @@ If session.yaml does not exist or has no language field, default to English.
290
303
 
291
304
  ## Load
292
305
 
293
- Read and execute the full orchestrator at \`chati.dev/orchestrator/chati.md\`.
306
+ Read and execute the full orchestrator at \`${orchestratorPath || 'chati.dev/orchestrator/chati.md'}\`.
294
307
 
295
308
  Pass through all context: session state, handoffs, artifacts, and user input.
296
309
 
@@ -316,7 +329,7 @@ Pass through all context: session state, handoffs, artifacts, and user input.
316
329
  } else if (ideKey === 'codex-cli') {
317
330
  // Codex CLI: chati skill via .agents/skills/chati/SKILL.md (invoke with $chati)
318
331
  createDir(join(targetDir, '.agents', 'skills', 'chati'));
319
- writeFileSync(join(targetDir, '.agents', 'skills', 'chati', 'SKILL.md'), generateCodexSkill(), 'utf-8');
332
+ writeFileSync(join(targetDir, '.agents', 'skills', 'chati', 'SKILL.md'), generateCodexSkill({ orchestratorPath }), 'utf-8');
320
333
 
321
334
  // Session lock override file (equivalent to CLAUDE.local.md)
322
335
  writeFileSync(join(targetDir, 'AGENTS.override.md'), generateAgentsOverrideMd(), 'utf-8');
@@ -327,7 +340,7 @@ Pass through all context: session state, handoffs, artifacts, and user input.
327
340
  writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
328
341
  } else if (ideKey === 'gemini-cli') {
329
342
  // Gemini CLI: TOML command file (native format for /chati command)
330
- writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter(), 'utf-8');
343
+ writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter({ orchestratorPath }), 'utf-8');
331
344
 
332
345
  // Context files via @import (equivalent to .claude/rules/chati/)
333
346
  const geminiContextDir = join(targetDir, '.gemini', 'context');
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @fileoverview Provider Overlay Generator for multi-CLI continuity.
3
+ *
4
+ * When multiple CLI providers are selected (e.g. Claude Code + Gemini CLI),
5
+ * the primary provider's adapted files go in chati.dev/ (main). For each
6
+ * secondary provider, this module generates an overlay directory at
7
+ * chati.dev/.adapted/<provider>/ with the ADAPTABLE_FILES adapted for
8
+ * that provider. Each CLI reads only its own files — zero runtime translation.
9
+ *
10
+ * Constitution Article XIX — multi-CLI governance.
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+ import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
16
+
17
+ /**
18
+ * Generate adapted framework overlays for secondary providers.
19
+ *
20
+ * For each non-primary CLI provider, creates chati.dev/.adapted/<provider>/
21
+ * with provider-adapted copies of all ADAPTABLE_FILES.
22
+ *
23
+ * Reads CANONICAL (Claude) content from frameworkSource, then adapts for
24
+ * each secondary provider. This avoids double-adaptation issues when the
25
+ * primary provider is not Claude.
26
+ *
27
+ * @param {string} targetDir - Project root directory
28
+ * @param {string} frameworkSource - Source directory with canonical framework files
29
+ * @param {string} primaryProvider - Primary provider name ('claude', 'gemini', 'codex')
30
+ * @param {string[]} allProviders - All CLI provider names
31
+ * @returns {{ generated: string[], skipped: string[] }}
32
+ */
33
+ export function generateProviderOverlays(targetDir, frameworkSource, primaryProvider, allProviders) {
34
+ const result = { generated: [], skipped: [] };
35
+ const frameworkDir = join(targetDir, 'chati.dev');
36
+
37
+ for (const provider of allProviders) {
38
+ if (provider === primaryProvider) {
39
+ result.skipped.push(provider);
40
+ continue;
41
+ }
42
+
43
+ const overlayDir = join(frameworkDir, '.adapted', provider);
44
+
45
+ for (const file of ADAPTABLE_FILES) {
46
+ const srcPath = join(frameworkSource, file);
47
+ if (!existsSync(srcPath)) continue;
48
+
49
+ // Read CANONICAL (Claude) content from source, then adapt for target provider
50
+ const canonicalContent = readFileSync(srcPath, 'utf-8');
51
+ const adapted = adaptFrameworkFile(canonicalContent, file, provider);
52
+
53
+ const destPath = join(overlayDir, file);
54
+ mkdirSync(dirname(destPath), { recursive: true });
55
+ writeFileSync(destPath, adapted, 'utf-8');
56
+ }
57
+
58
+ result.generated.push(provider);
59
+ }
60
+
61
+ return result;
62
+ }
63
+
64
+ /**
65
+ * Resolve the correct framework file path for a given provider.
66
+ *
67
+ * Returns the overlay path for secondary providers (if it exists),
68
+ * otherwise falls back to the main chati.dev/ path.
69
+ *
70
+ * @param {string} projectDir - Project root directory
71
+ * @param {string} relativePath - Path relative to chati.dev/ (e.g. 'agents/build/dev.md')
72
+ * @param {string} provider - Target provider name
73
+ * @returns {string} Absolute path to the adapted file
74
+ */
75
+ export function resolveOverlayPath(projectDir, relativePath, provider) {
76
+ // Check overlay first
77
+ const overlayPath = join(projectDir, 'chati.dev', '.adapted', provider, relativePath);
78
+ if (existsSync(overlayPath)) return overlayPath;
79
+
80
+ // Fallback to main chati.dev/
81
+ return join(projectDir, 'chati.dev', relativePath);
82
+ }
@@ -4,9 +4,10 @@ import yaml from 'js-yaml';
4
4
  * Generate session.yaml content
5
5
  */
6
6
  export function generateSessionYaml(config) {
7
- const { projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider } = config;
7
+ const { projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders } = config;
8
8
 
9
9
  const session = {
10
+ schema_version: '1.0',
10
11
  project: {
11
12
  name: projectName,
12
13
  type: projectType,
@@ -18,7 +19,9 @@ export function generateSessionYaml(config) {
18
19
  language: language,
19
20
  ides: selectedIDEs,
20
21
  mcps: selectedMCPs,
21
- providers_enabled: [llmProvider || 'claude'],
22
+ providers_enabled: allProviders && allProviders.length > 0 ? allProviders : [llmProvider || 'claude'],
23
+ primary_provider: llmProvider || 'claude',
24
+ active_provider: llmProvider || 'claude',
22
25
  user_level: 'auto',
23
26
  user_level_confidence: 0.0,
24
27
  agents: {},
@@ -86,8 +89,9 @@ export const PROVIDER_MODEL_MAPS = {
86
89
  * Generate config.yaml content
87
90
  */
88
91
  export function generateConfigYaml(config) {
89
- const { version, projectType, language, selectedIDEs, llmProvider } = config;
92
+ const { version, projectType, language, selectedIDEs, llmProvider, allProviders } = config;
90
93
  const provider = llmProvider || 'claude';
94
+ const providers = allProviders && allProviders.length > 0 ? allProviders : [provider];
91
95
 
92
96
  const configData = {
93
97
  version: version,
@@ -98,12 +102,18 @@ export function generateConfigYaml(config) {
98
102
  language: language,
99
103
  ides: selectedIDEs,
100
104
  providers: {
101
- claude: { enabled: true, primary: provider === 'claude' },
102
- gemini: { enabled: provider === 'gemini' || selectedIDEs.includes('gemini-cli'), model_default: 'pro', primary: provider === 'gemini' },
103
- codex: { enabled: provider === 'codex' || selectedIDEs.includes('codex-cli'), model_default: 'codex', primary: provider === 'codex' },
105
+ claude: { enabled: providers.includes('claude') || provider === 'claude', primary: provider === 'claude' },
106
+ gemini: { enabled: providers.includes('gemini') || selectedIDEs.includes('gemini-cli'), model_default: 'pro', primary: provider === 'gemini' },
107
+ codex: { enabled: providers.includes('codex') || selectedIDEs.includes('codex-cli'), model_default: 'codex', primary: provider === 'codex' },
104
108
  },
105
109
  };
106
110
 
111
+ // Add telemetry config
112
+ configData.telemetry = {
113
+ enabled: config.telemetryEnabled || false,
114
+ anonymous_id: null,
115
+ };
116
+
107
117
  // Add agent_overrides with optimal model map for the selected provider
108
118
  if (provider !== 'claude') {
109
119
  const modelMap = PROVIDER_MODEL_MAPS[provider];
@@ -144,7 +154,8 @@ Type \`/chati\` to start.
144
154
  * The user invokes with $chati or Codex auto-detects based on description.
145
155
  * Also reads AGENTS.md for project context (auto-generated by context-file-generator).
146
156
  */
147
- export function generateCodexSkill() {
157
+ export function generateCodexSkill(options = {}) {
158
+ const orchPath = options.orchestratorPath || 'chati.dev/orchestrator/chati.md';
148
159
  return `---
149
160
  name: chati
150
161
  description: >
@@ -175,7 +186,7 @@ If session.yaml does not exist or has no language field, default to English.
175
186
  The orchestrator and all agent files are **pre-configured for Codex CLI**.
176
187
  No translation needed — follow all instructions as written.
177
188
 
178
- Read and execute the full orchestrator at \`chati.dev/orchestrator/chati.md\`.
189
+ Read and execute the full orchestrator at \`${orchPath}\`.
179
190
 
180
191
  **NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.**
181
192
 
@@ -195,7 +206,8 @@ Read and execute the full orchestrator at \`chati.dev/orchestrator/chati.md\`.
195
206
  * Gemini CLI custom commands use TOML format with `description` and `prompt` fields.
196
207
  * Supports {{args}} for user arguments and @{file} for file injection.
197
208
  */
198
- export function generateGeminiRouter() {
209
+ export function generateGeminiRouter(options = {}) {
210
+ const orchPath = options.orchestratorPath || 'chati.dev/orchestrator/chati.md';
199
211
  return `description = "Activate Chati.dev orchestrator"
200
212
  prompt = """
201
213
  CRITICAL — Language Override:
@@ -206,7 +218,7 @@ If session.yaml does not exist or has no language field, default to English.
206
218
  The orchestrator and all agent files are pre-configured for Gemini CLI.
207
219
  No translation needed — follow all instructions as written.
208
220
 
209
- Read and execute the full orchestrator at chati.dev/orchestrator/chati.md.
221
+ Read and execute the full orchestrator at ${orchPath}.
210
222
  NEVER create or reference CLAUDE.md, CLAUDE.local.md, or .claude/ directories.
211
223
 
212
224
  Context to load:
@@ -210,8 +210,9 @@ export class InstallTransaction {
210
210
  break;
211
211
  }
212
212
  }
213
- } catch {
214
- // Best-effort rollback: continue even if individual reversals fail
213
+ } catch (err) {
214
+ // Best-effort rollback: log and continue
215
+ console.error('[chati] rollback warning:', err?.message);
215
216
  }
216
217
  }
217
218