chati-dev 3.0.0 → 3.0.2

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 (70) hide show
  1. package/README.md +96 -24
  2. package/assets/logo2.png +0 -0
  3. package/assets/social-preview.png +0 -0
  4. package/bin/chati.js +1 -1
  5. package/framework/agents/plan/phases.md +2 -2
  6. package/framework/config.yaml +2 -2
  7. package/framework/constitution.md +4 -4
  8. package/framework/context/governance.md +16 -3
  9. package/framework/context/root.md +3 -3
  10. package/framework/data/entity-registry.yaml +1 -1
  11. package/framework/domains/agents/detail.yaml +1 -1
  12. package/framework/domains/agents/phases.yaml +2 -2
  13. package/framework/domains/agents/tasks.yaml +2 -2
  14. package/framework/domains/constitution.yaml +2 -2
  15. package/framework/hooks/read-protection.js +1 -0
  16. package/framework/i18n/en.yaml +11 -4
  17. package/framework/i18n/es.yaml +11 -4
  18. package/framework/i18n/fr.yaml +11 -4
  19. package/framework/i18n/pt.yaml +11 -4
  20. package/framework/intelligence/context-engine.md +4 -4
  21. package/framework/intelligence/decision-engine.md +1 -1
  22. package/framework/patterns/elicitation-library.yaml +2 -2
  23. package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
  24. package/framework/tasks/brownfield-wu-migration-plan.md +4 -4
  25. package/framework/tasks/brownfield-wu-risk-assess.md +2 -2
  26. package/framework/tasks/detail-expand-prd.md +2 -2
  27. package/framework/tasks/greenfield-wu-report.md +1 -1
  28. package/framework/tasks/orchestrator-handoff.md +2 -2
  29. package/framework/tasks/orchestrator-resume.md +1 -1
  30. package/framework/tasks/qa-impl-performance-test.md +2 -2
  31. package/framework/tasks/qa-impl-sast-scan.md +6 -6
  32. package/framework/tasks/qa-planning-consolidate.md +1 -1
  33. package/framework/tasks/qa-planning-coverage-plan.md +9 -9
  34. package/framework/tasks/qa-planning-gate-define.md +6 -6
  35. package/framework/tasks/qa-planning-risk-matrix.md +1 -1
  36. package/framework/tasks/tasks-consolidate.md +5 -5
  37. package/package.json +1 -1
  38. package/scripts/changelog-generator.js +2 -2
  39. package/scripts/ide-sync.js +9 -4
  40. package/scripts/pr-review.js +5 -5
  41. package/scripts/semantic-lint.js +3 -3
  42. package/src/autonomy/build-state.js +1 -1
  43. package/src/autonomy/execution-profile.js +1 -1
  44. package/src/config/context-file-generator.js +31 -34
  45. package/src/config/ide-configs.js +15 -6
  46. package/src/dashboard/layout.js +3 -1
  47. package/src/gates/g2-qa-planning.js +1 -1
  48. package/src/gates/g4-qa-implementation.js +1 -1
  49. package/src/health/engine.js +7 -12
  50. package/src/installer/core.js +69 -34
  51. package/src/installer/templates.js +168 -2
  52. package/src/installer/validator.js +5 -3
  53. package/src/intelligence/registry-manager.js +2 -2
  54. package/src/preview/detector.js +1 -1
  55. package/src/terminal/adapters/claude-adapter.js +3 -13
  56. package/src/terminal/adapters/codex-adapter.js +3 -13
  57. package/src/terminal/adapters/copilot-adapter.js +3 -13
  58. package/src/terminal/adapters/gemini-adapter.js +3 -17
  59. package/src/terminal/cli-registry.js +20 -48
  60. package/src/terminal/index.js +1 -0
  61. package/src/terminal/prompt-builder.js +3 -18
  62. package/src/terminal/run-agent.js +2 -1
  63. package/src/terminal/spawner.js +70 -8
  64. package/src/terminal/wave-analyzer.js +1 -1
  65. package/src/utils/config-parser.js +97 -0
  66. package/src/wizard/feedback.js +1 -1
  67. package/src/wizard/i18n.js +11 -4
  68. package/src/wizard/index.js +45 -15
  69. package/src/wizard/questions.js +54 -3
  70. package/assets/logo - c/303/263pia.svg" +0 -42
@@ -6,9 +6,8 @@
6
6
  * truth for multi-CLI governance (Constitution Article XIX).
7
7
  */
8
8
 
9
- import { existsSync, readFileSync } from 'fs';
10
- import { join } from 'path';
11
9
  import * as adapters from './adapters/index.js';
10
+ import { parseProviderConfig, parseAgentOverride } from '../utils/config-parser.js';
12
11
 
13
12
  // ---------------------------------------------------------------------------
14
13
  // Provider Definitions
@@ -49,10 +48,10 @@ const PROVIDERS = {
49
48
  gemini: {
50
49
  name: 'gemini',
51
50
  command: 'gemini',
52
- baseArgs: ['--prompt'],
51
+ baseArgs: [],
53
52
  modelFlag: '--model',
54
53
  stdinSupport: true,
55
- hooksSupport: true,
54
+ hooksSupport: false,
56
55
  mcpSupport: true,
57
56
  contextFile: 'GEMINI.md',
58
57
  modelMap: {
@@ -72,7 +71,7 @@ const PROVIDERS = {
72
71
  contextFile: 'AGENTS.md',
73
72
  modelMap: {
74
73
  codex: 'gpt-5.3-codex',
75
- mini: 'gpt-5.1-codex-mini',
74
+ mini: 'codex-mini-latest',
76
75
  },
77
76
  adapter: adapters.codex,
78
77
  },
@@ -82,12 +81,12 @@ const PROVIDERS = {
82
81
  baseArgs: ['-p'],
83
82
  modelFlag: '--model',
84
83
  stdinSupport: true,
85
- hooksSupport: true,
84
+ hooksSupport: false,
86
85
  mcpSupport: true,
87
86
  contextFile: null,
88
87
  modelMap: {
89
88
  'claude-sonnet': 'claude-sonnet-4.5',
90
- 'gpt-5': 'gpt-5.1',
89
+ 'gpt-5': 'gpt-5',
91
90
  },
92
91
  adapter: adapters.copilot,
93
92
  },
@@ -128,33 +127,7 @@ export function getAllProviders() {
128
127
  * @returns {{ primary: string, enabled: string[] }}
129
128
  */
130
129
  export function loadEnabledProviders(projectDir) {
131
- const configPath = join(projectDir, 'chati.dev', 'config.yaml');
132
- if (!existsSync(configPath)) {
133
- return { primary: 'claude', enabled: ['claude'] };
134
- }
135
-
136
- const raw = readFileSync(configPath, 'utf-8');
137
-
138
- // Lightweight YAML extraction (avoid dependency in spawning path)
139
- const enabled = [];
140
- let primary = 'claude';
141
-
142
- for (const name of Object.keys(PROVIDERS)) {
143
- const enabledMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?enabled:\\s*(true|false)`, 'm'));
144
- if (enabledMatch && enabledMatch[1] === 'true') {
145
- enabled.push(name);
146
- }
147
- const primaryMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?primary:\\s*(true|false)`, 'm'));
148
- if (primaryMatch && primaryMatch[1] === 'true') {
149
- primary = name;
150
- }
151
- }
152
-
153
- // Claude is always enabled as fallback
154
- if (!enabled.includes('claude')) {
155
- enabled.unshift('claude');
156
- }
157
-
130
+ const { primary, enabled } = parseProviderConfig(projectDir);
158
131
  return { primary, enabled };
159
132
  }
160
133
 
@@ -170,16 +143,12 @@ export function loadEnabledProviders(projectDir) {
170
143
  export function resolveProviderForAgent(agent, projectDir, agentModels) {
171
144
  const { primary, enabled } = loadEnabledProviders(projectDir);
172
145
 
173
- // Check agent_overrides in config.yaml
174
- const configPath = join(projectDir, 'chati.dev', 'config.yaml');
175
- if (existsSync(configPath)) {
176
- const raw = readFileSync(configPath, 'utf-8');
177
- const overrideMatch = raw.match(new RegExp(`${agent}:\\s*\\{[^}]*provider:\\s*(\\w+)[^}]*model:\\s*(\\w+)`, 'm'));
178
- if (overrideMatch) {
179
- const overrideProvider = overrideMatch[1];
180
- if (enabled.includes(overrideProvider)) {
181
- return { provider: overrideProvider, model: overrideMatch[2] };
182
- }
146
+ // Check agent_overrides in config.yaml (block-style YAML)
147
+ const { raw } = parseProviderConfig(projectDir);
148
+ if (raw) {
149
+ const override = parseAgentOverride(raw, agent);
150
+ if (override && enabled.includes(override.provider)) {
151
+ return override;
183
152
  }
184
153
  }
185
154
 
@@ -192,8 +161,10 @@ export function resolveProviderForAgent(agent, projectDir, agentModels) {
192
161
  }
193
162
  }
194
163
 
195
- // Fallback to primary
196
- return { provider: primary, model: 'sonnet' };
164
+ // Fallback to primary provider with its default light-tier model
165
+ const provider = PROVIDERS[primary];
166
+ const defaultModel = provider ? Object.keys(provider.modelMap)[0] : 'sonnet';
167
+ return { provider: primary, model: defaultModel };
197
168
  }
198
169
 
199
170
  /**
@@ -206,9 +177,10 @@ export async function isProviderAvailable(name) {
206
177
  const provider = PROVIDERS[name];
207
178
  if (!provider) return false;
208
179
 
209
- const { execSync } = await import('child_process');
180
+ const { execFileSync } = await import('child_process');
210
181
  try {
211
- execSync(`which ${provider.command}`, { stdio: 'ignore' });
182
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
183
+ execFileSync(cmd, [provider.command], { stdio: 'ignore' });
212
184
  return true;
213
185
  } catch {
214
186
  return false;
@@ -8,6 +8,7 @@ export {
8
8
  spawnParallelGroup,
9
9
  killTerminal,
10
10
  getTerminalStatus,
11
+ cleanParentEnv,
11
12
  _resetCounter,
12
13
  } from './spawner.js';
13
14
 
@@ -14,24 +14,9 @@ import { runPrism } from '../context/engine.js';
14
14
  import { loadHandoff, formatHandoff } from '../tasks/handoff.js';
15
15
  import { getWriteScope } from './isolation.js';
16
16
 
17
- // Re-read at import time so the prompt builder always uses the correct map
18
- // without circular dependency issues (the hook file runs main() on import
19
- // which writes to stdout — we only need the data, so we inline the map).
20
- const AGENT_MODELS = {
21
- orchestrator: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
22
- 'greenfield-wu': { provider: 'claude', model: 'haiku', tier: 'haiku' },
23
- 'brownfield-wu': { provider: 'claude', model: 'opus', tier: 'opus' },
24
- brief: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
25
- detail: { provider: 'claude', model: 'opus', tier: 'opus' },
26
- architect: { provider: 'claude', model: 'opus', tier: 'opus' },
27
- ux: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
28
- phases: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
29
- tasks: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
30
- 'qa-planning': { provider: 'claude', model: 'opus', tier: 'opus' },
31
- 'qa-implementation': { provider: 'claude', model: 'opus', tier: 'opus' },
32
- dev: { provider: 'claude', model: 'opus', tier: 'opus' },
33
- devops: { provider: 'claude', model: 'sonnet', tier: 'sonnet' },
34
- };
17
+ // Import AGENT_MODELS from model-governance (safe named export,
18
+ // does not trigger main() which is guarded by fileURLToPath check).
19
+ import { AGENT_MODELS } from '../../framework/hooks/model-governance.js';
35
20
 
36
21
  /**
37
22
  * Map of agent names to their definition file paths (relative to project root).
@@ -160,7 +160,8 @@ async function main() {
160
160
  function waitForExit(handle, timeout) {
161
161
  return new Promise((resolve, reject) => {
162
162
  if (handle.status === 'exited') {
163
- return resolve();
163
+ resolve();
164
+ return;
164
165
  }
165
166
 
166
167
  const timer = setTimeout(() => {
@@ -35,6 +35,48 @@ export function _resetCounter() {
35
35
  _counter = 0;
36
36
  }
37
37
 
38
+ // ---------------------------------------------------------------------------
39
+ // Environment cleaning
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /**
43
+ * Environment variable prefixes injected by Claude Code that must NOT
44
+ * leak into spawned CLI processes. A spawned `claude --print` that
45
+ * inherits `CLAUDECODE=1` will refuse to start ("cannot be launched
46
+ * inside another Claude Code session").
47
+ *
48
+ * @type {string[]}
49
+ */
50
+ const BLOCKED_ENV_PREFIXES = ['CLAUDE_CODE_', 'CLAUDE_AGENT_SDK_'];
51
+
52
+ /**
53
+ * Exact environment variable names to strip (no prefix match needed).
54
+ * @type {string[]}
55
+ */
56
+ const BLOCKED_ENV_EXACT = ['CLAUDECODE'];
57
+
58
+ /**
59
+ * Return a copy of the given environment with Claude Code-specific
60
+ * variables removed. This prevents "nested session" errors when
61
+ * spawning `claude --print` from inside a Claude Code session.
62
+ *
63
+ * The function is intentionally conservative — it only removes vars
64
+ * that are known to be injected by the Claude Code host process.
65
+ * User-configured vars like CLAUDE_API_KEY are preserved.
66
+ *
67
+ * @param {Record<string, string>} env - Source environment (typically process.env)
68
+ * @returns {Record<string, string>} Cleaned environment (new object)
69
+ */
70
+ export function cleanParentEnv(env) {
71
+ const cleaned = {};
72
+ for (const [key, value] of Object.entries(env)) {
73
+ if (BLOCKED_ENV_EXACT.includes(key)) continue;
74
+ if (BLOCKED_ENV_PREFIXES.some(prefix => key.startsWith(prefix))) continue;
75
+ cleaned[key] = value;
76
+ }
77
+ return cleaned;
78
+ }
79
+
38
80
  // ---------------------------------------------------------------------------
39
81
  // Public API
40
82
  // ---------------------------------------------------------------------------
@@ -43,7 +85,8 @@ export function _resetCounter() {
43
85
  * @typedef {object} SpawnConfig
44
86
  * @property {string} agent - Agent name (e.g. "architect")
45
87
  * @property {string} taskId - Task identifier
46
- * @property {string} [model] - LLM model (haiku/sonnet/opus)
88
+ * @property {string} [model] - LLM model tier name (e.g. opus, pro, codex, claude-sonnet)
89
+ * @property {string} [provider] - CLI provider name (claude, gemini, codex, copilot)
47
90
  * @property {string} [prompt] - Full prompt string (from prompt-builder, piped via stdin)
48
91
  * @property {object} [contextPayload] - Context to inject via env var
49
92
  * @property {string[]} [writeScope] - Override write scope
@@ -114,12 +157,15 @@ export function buildSpawnCommand(config) {
114
157
  command = adapterResult.command;
115
158
  args = adapterResult.args;
116
159
  prompt = adapterResult.stdinPrompt;
117
- } catch {
160
+ } catch (err) {
118
161
  // Fallback to claude if provider resolution fails (backwards compatibility)
162
+ console.error(`[chati] Provider "${providerName}" resolution failed: ${err.message}. Falling back to claude.`);
119
163
  command = 'claude';
120
164
  args = ['--print', '--dangerously-skip-permissions'];
121
165
  if (config.model) {
122
- args.push('--model', config.model);
166
+ const claudeProvider = getProvider('claude');
167
+ const resolvedModel = claudeProvider.modelMap[config.model] || config.model;
168
+ args.push('--model', resolvedModel);
123
169
  }
124
170
  prompt = config.prompt || null;
125
171
  }
@@ -141,7 +187,7 @@ export function spawnTerminal(config) {
141
187
 
142
188
  const child = spawn(command, args, {
143
189
  cwd,
144
- env: { ...process.env, ...env },
190
+ env: { ...cleanParentEnv(process.env), ...env },
145
191
  stdio: ['pipe', 'pipe', 'pipe'],
146
192
  });
147
193
 
@@ -157,7 +203,7 @@ export function spawnTerminal(config) {
157
203
  process: child,
158
204
  agent: config.agent,
159
205
  taskId: config.taskId,
160
- model: config.model || 'sonnet',
206
+ model: config.model || 'unknown',
161
207
  startedAt: new Date().toISOString(),
162
208
  status: 'running',
163
209
  exitCode: null,
@@ -166,15 +212,20 @@ export function spawnTerminal(config) {
166
212
  timeout,
167
213
  };
168
214
 
169
- // Capture output
215
+ // Capture output (capped at ~10MB to prevent unbounded memory growth)
216
+ const MAX_BUFFER_CHUNKS = 10_000;
170
217
  if (child.stdout) {
171
218
  child.stdout.on('data', (chunk) => {
172
- handle.stdout.push(chunk.toString());
219
+ if (handle.stdout.length < MAX_BUFFER_CHUNKS) {
220
+ handle.stdout.push(chunk.toString());
221
+ }
173
222
  });
174
223
  }
175
224
  if (child.stderr) {
176
225
  child.stderr.on('data', (chunk) => {
177
- handle.stderr.push(chunk.toString());
226
+ if (handle.stderr.length < MAX_BUFFER_CHUNKS) {
227
+ handle.stderr.push(chunk.toString());
228
+ }
178
229
  });
179
230
  }
180
231
 
@@ -189,6 +240,17 @@ export function spawnTerminal(config) {
189
240
  handle.stderr.push(`spawn error: ${err.message}`);
190
241
  });
191
242
 
243
+ // Enforce timeout — kill process if it exceeds max execution time
244
+ const timeoutTimer = setTimeout(() => {
245
+ if (handle.status === 'running') {
246
+ handle.stderr.push(`timeout: process exceeded ${timeout}ms limit`);
247
+ killTerminal(handle);
248
+ }
249
+ }, timeout);
250
+
251
+ // Clear timer when process exits normally
252
+ child.on('exit', () => clearTimeout(timeoutTimer));
253
+
192
254
  return handle;
193
255
  }
194
256
 
@@ -59,7 +59,7 @@ export function analyzeWaves(tasks) {
59
59
 
60
60
  // Kahn's algorithm with wave grouping
61
61
  const waves = [];
62
- let remaining = new Set(tasks.map((t) => t.id));
62
+ const remaining = new Set(tasks.map((t) => t.id));
63
63
 
64
64
  while (remaining.size > 0) {
65
65
  // Find all tasks with no pending dependencies (in-degree 0)
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @fileoverview Shared YAML config parser for provider configuration.
3
+ *
4
+ * Extracts provider enabled/primary status and agent overrides from
5
+ * config.yaml using lightweight regex-based parsing. Used by
6
+ * cli-registry.js, context-file-generator.js, and health/engine.js.
7
+ */
8
+
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+
12
+ /**
13
+ * Parse provider configuration from config.yaml.
14
+ *
15
+ * @param {string} projectDir - Project root directory
16
+ * @returns {{ primary: string, enabled: string[], raw: string|null }}
17
+ */
18
+ export function parseProviderConfig(projectDir) {
19
+ const configPath = join(projectDir, 'chati.dev', 'config.yaml');
20
+ if (!existsSync(configPath)) {
21
+ return { primary: 'claude', enabled: ['claude'], raw: null };
22
+ }
23
+
24
+ const raw = readFileSync(configPath, 'utf-8');
25
+ const providers = ['claude', 'gemini', 'codex', 'copilot'];
26
+ const enabled = [];
27
+ let primary = 'claude';
28
+
29
+ for (const name of providers) {
30
+ const enabledMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?enabled:\\s*(true|false)`, 'm'));
31
+ if (enabledMatch && enabledMatch[1] === 'true') {
32
+ enabled.push(name);
33
+ }
34
+ const primaryMatch = raw.match(new RegExp(`${name}:[\\s\\S]*?primary:\\s*(true|false)`, 'm'));
35
+ if (primaryMatch && primaryMatch[1] === 'true') {
36
+ primary = name;
37
+ }
38
+ }
39
+
40
+ // Claude is always enabled as fallback
41
+ if (!enabled.includes('claude')) {
42
+ enabled.unshift('claude');
43
+ }
44
+
45
+ return { primary, enabled, raw };
46
+ }
47
+
48
+ /**
49
+ * Parse agent override block from config.yaml.
50
+ *
51
+ * @param {string} raw - Raw config.yaml content
52
+ * @param {string} agent - Agent name
53
+ * @returns {{ provider: string, model: string }|null}
54
+ */
55
+ export function parseAgentOverride(raw, agent) {
56
+ const escaped = agent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
57
+ const overrideBlock = raw.match(new RegExp(`^\\s+${escaped}:\\s*\\n((?:\\s+\\w+:.*\\n?)*)`, 'm'));
58
+ if (!overrideBlock) return null;
59
+
60
+ const block = overrideBlock[1];
61
+ const providerMatch = block.match(/provider:\s*(\S+)/);
62
+ const modelMatch = block.match(/model:\s*(\S+)/);
63
+
64
+ if (providerMatch && modelMatch) {
65
+ return { provider: providerMatch[1], model: modelMatch[1] };
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Get list of enabled non-Claude providers.
72
+ *
73
+ * @param {string} projectDir - Project root directory
74
+ * @returns {string[]} List of enabled provider names (excludes claude)
75
+ */
76
+ export function getEnabledNonClaudeProviders(projectDir) {
77
+ const { enabled } = parseProviderConfig(projectDir);
78
+ return enabled.filter(p => p !== 'claude');
79
+ }
80
+
81
+ /**
82
+ * Check if a CLI command is available on the system.
83
+ * Cross-platform: uses 'which' on Unix, 'where' on Windows.
84
+ *
85
+ * @param {string} command - CLI command name
86
+ * @returns {Promise<boolean>}
87
+ */
88
+ export async function isCommandAvailable(command) {
89
+ const { execFileSync } = await import('child_process');
90
+ try {
91
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
92
+ execFileSync(cmd, [command], { stdio: 'ignore' });
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
@@ -60,7 +60,7 @@ export function showQuickStart(title, steps) {
60
60
  export function showSummary(data) {
61
61
  console.log();
62
62
  for (const [label, value] of Object.entries(data)) {
63
- const paddedLabel = label.padEnd(12);
63
+ const paddedLabel = label.padEnd(16);
64
64
  console.log(` ${dim(paddedLabel)} ${value}`);
65
65
  }
66
66
  }
@@ -20,19 +20,26 @@ const FALLBACK_EN = {
20
20
  language_label: 'Language',
21
21
  ides_label: 'IDEs',
22
22
  mcps_label: 'MCPs',
23
+ llm_provider_title: 'Select your default AI provider:',
24
+ llm_provider_claude: 'Claude (Anthropic) — Best for structured reasoning',
25
+ llm_provider_gemini: 'Gemini (Google) — 1M token context window',
26
+ llm_provider_codex: 'Codex (OpenAI) — Optimized for code generation',
27
+ llm_provider_copilot: 'Copilot (GitHub) — Multi-model, GitHub integrated',
28
+ llm_provider_not_installed: '(CLI not detected)',
29
+ llm_provider_label: 'AI Provider',
23
30
  will_install: 'Will install:',
24
31
  agents_count: '13 agent definitions (DISCOVER, PLAN, BUILD, DEPLOY phases)',
25
32
  workflows_count: '6 workflow blueprints',
26
33
  templates_count: '6 templates (PRD, Brownfield PRD, Architecture, Task, QA Gate, Quick Brief)',
27
- constitution: 'Constitution (17 Articles + Preamble)',
34
+ constitution: 'Constitution (19 Articles + Preamble)',
28
35
  session_mgmt: 'Session management system',
29
- quality_gates: 'Quality gates (4-tier validation)',
36
+ quality_gates: 'Quality gates (3-tier verdicts)',
30
37
  proceed: 'Proceed with installation?',
31
38
  installing: 'Installing Chati.dev...',
32
39
  created_chati: 'Created .chati/ session directory',
33
40
  created_framework: 'Created chati.dev/ system directory (agents, templates, workflows)',
34
41
  created_commands: 'Created .claude/commands/ (thin router)',
35
- installed_constitution: 'Installed Constitution (Articles I-XVII)',
42
+ installed_constitution: 'Installed Constitution (Articles I-XIX)',
36
43
  created_session: 'Created session.yaml schema',
37
44
  created_claude_md: 'Created CLAUDE.md',
38
45
  configured_mcps: 'Configured MCPs:',
@@ -40,7 +47,7 @@ const FALLBACK_EN = {
40
47
  agents_valid: 'All 13 agents implement 8 protocols',
41
48
  handoff_ok: 'Handoff protocol: OK',
42
49
  validation_ok: 'Self-validation criteria: OK',
43
- constitution_ok: 'Constitution: 17 articles verified',
50
+ constitution_ok: 'Constitution: 19 articles verified',
44
51
  created_memories: 'Created .chati/memories/ (Memory Layer)',
45
52
  installed_intelligence: 'Installed Intelligence Layer (Context Engine, Memory, Registry)',
46
53
  intelligence_valid: 'Intelligence: 6 spec files verified',
@@ -3,12 +3,24 @@ import { readFileSync } from 'fs';
3
3
  import { join, dirname, basename } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { logBanner } from '../utils/logger.js';
6
- import { stepLanguage, stepProjectType, stepConfirmation } from './questions.js';
6
+ import { stepLanguage, stepProjectType, stepLlmProvider, stepConfirmation } from './questions.js';
7
7
  import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
9
  import { validateInstallation } from '../installer/validator.js';
10
10
  import { t } from './i18n.js';
11
11
  import { DEFAULT_MCPS } from '../config/mcp-configs.js';
12
+ import { IDE_CONFIGS } from '../config/ide-configs.js';
13
+
14
+ /**
15
+ * Map LLM provider to its primary IDE.
16
+ * Each provider maps to its own standalone IDE — no cross-contamination.
17
+ */
18
+ const PROVIDER_TO_IDE = {
19
+ claude: ['claude-code'],
20
+ gemini: ['gemini-cli'],
21
+ codex: ['codex-cli'],
22
+ copilot: ['github-copilot'],
23
+ };
12
24
 
13
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
26
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
@@ -35,16 +47,20 @@ export async function runWizard(targetDir, options = {}) {
35
47
  // Step 2: Project Type
36
48
  const projectType = options.projectType || await stepProjectType(targetDir);
37
49
 
38
- // Auto-configured: Claude Code + default MCPs
39
- const selectedIDEs = options.ides || ['claude-code'];
50
+ // Step 3: LLM Provider
51
+ const llmProvider = options.llmProvider || await stepLlmProvider();
52
+
53
+ // Map provider to its standalone IDE(s)
54
+ const selectedIDEs = options.ides || PROVIDER_TO_IDE[llmProvider] || ['claude-code'];
40
55
  const selectedMCPs = options.mcps || DEFAULT_MCPS;
41
56
 
42
- // Step 3: Confirmation
57
+ // Step 4: Confirmation
43
58
  const projectName = basename(targetDir);
44
59
  const config = {
45
60
  projectName,
46
61
  projectType,
47
62
  language,
63
+ llmProvider,
48
64
  selectedIDEs,
49
65
  selectedMCPs,
50
66
  targetDir,
@@ -53,7 +69,7 @@ export async function runWizard(targetDir, options = {}) {
53
69
 
54
70
  await stepConfirmation(config);
55
71
 
56
- // Step 4: Installation + Validation
72
+ // Step 5: Installation + Validation
57
73
  console.log();
58
74
  const installSpinner = createSpinner(t('installer.installing'));
59
75
  installSpinner.start();
@@ -67,7 +83,9 @@ export async function runWizard(targetDir, options = {}) {
67
83
  showStep(t('installer.created_commands'));
68
84
  showStep(t('installer.installed_constitution'));
69
85
  showStep(t('installer.created_session'));
70
- showStep(t('installer.created_claude_md'));
86
+ if (selectedIDEs.includes('claude-code')) {
87
+ showStep(t('installer.created_claude_md'));
88
+ }
71
89
  showStep(t('installer.created_memories'));
72
90
  showStep(t('installer.installed_intelligence'));
73
91
  showStep(`${t('installer.configured_mcps')} ${selectedMCPs.join(', ')}`);
@@ -80,28 +98,40 @@ export async function runWizard(targetDir, options = {}) {
80
98
  const validation = await validateInstallation(targetDir);
81
99
  validateSpinner.stop();
82
100
 
83
- showValidation(t('installer.agents_valid'));
101
+ // Show validation results based on actual checks
102
+ if (validation.agents?.pass) showValidation(t('installer.agents_valid'));
103
+ if (validation.constitution?.pass) showValidation(t('installer.constitution_ok'));
104
+ if (validation.intelligence?.pass) showValidation(t('installer.intelligence_valid'));
105
+ if (validation.registry?.pass) showValidation(t('installer.registry_valid'));
106
+ if (validation.memories?.pass) showValidation(t('installer.memories_valid'));
107
+ if (validation.session?.pass) showValidation(t('installer.session_ok'));
84
108
  showValidation(t('installer.handoff_ok'));
85
109
  showValidation(t('installer.validation_ok'));
86
- showValidation(t('installer.constitution_ok'));
87
- showValidation(t('installer.intelligence_valid'));
88
- showValidation(t('installer.registry_valid'));
89
- showValidation(t('installer.memories_valid'));
90
- showValidation(t('installer.session_ok'));
110
+
111
+ // Warn if any checks failed
112
+ if (validation.passed < validation.total) {
113
+ const failed = validation.total - validation.passed;
114
+ p.log.warn(`${failed} validation check(s) did not pass. Run 'npx chati-dev health' for details.`);
115
+ }
91
116
 
92
117
  console.log();
93
118
  p.outro(t('installer.success'));
94
119
 
120
+ // Show quick start — same experience across all providers
121
+ const primaryIDE = selectedIDEs[selectedIDEs.length - 1];
122
+ const primaryIDEName = IDE_CONFIGS[primaryIDE]?.name || primaryIDE;
123
+ const invokeCmd = primaryIDE === 'github-copilot' ? '@chati' : '/chati';
95
124
  showQuickStart(t('installer.quick_start_title'), [
96
- `${t('installer.quick_start_1')} (Claude Code)`,
97
- t('installer.quick_start_2'),
125
+ `${t('installer.quick_start_1')} (${primaryIDEName})`,
126
+ `Type: ${invokeCmd}`,
98
127
  t('installer.quick_start_3'),
99
128
  ]);
100
129
 
101
130
  return { success: true, config, validation };
102
131
  } catch (err) {
103
132
  installSpinner.stop();
104
- p.cancel(`Installation failed: ${err.message}`);
133
+ const phase = err.message?.includes('validat') ? 'Validation' : 'Installation';
134
+ p.cancel(`${phase} failed: ${err.message}`);
105
135
  return { success: false, error: err.message };
106
136
  }
107
137
  }
@@ -3,6 +3,8 @@ import { t, SUPPORTED_LANGUAGES, loadLanguage } from './i18n.js';
3
3
  import { detectProjectType } from '../utils/detector.js';
4
4
  import { showSummary, showChecklist } from './feedback.js';
5
5
  import { brand, dim, success } from '../utils/colors.js';
6
+ import { isProviderAvailable } from '../terminal/cli-registry.js';
7
+ import { IDE_CONFIGS } from '../config/ide-configs.js';
6
8
 
7
9
  /**
8
10
  * Step 1: Language Selection (always in English)
@@ -58,13 +60,61 @@ export async function stepProjectType(targetDir) {
58
60
  }
59
61
 
60
62
  /**
61
- * Step 3: Confirmation
63
+ * Step 3: LLM Provider Selection
64
+ */
65
+ export async function stepLlmProvider() {
66
+ const providers = [
67
+ { name: 'claude', label: t('installer.llm_provider_claude') },
68
+ { name: 'gemini', label: t('installer.llm_provider_gemini') },
69
+ { name: 'codex', label: t('installer.llm_provider_codex') },
70
+ { name: 'copilot', label: t('installer.llm_provider_copilot') },
71
+ ];
72
+
73
+ // Check availability of each provider
74
+ const availability = {};
75
+ for (const prov of providers) {
76
+ try {
77
+ availability[prov.name] = await isProviderAvailable(prov.name);
78
+ } catch {
79
+ availability[prov.name] = prov.name === 'claude'; // claude always available
80
+ }
81
+ }
82
+
83
+ const options = providers.map(prov => {
84
+ const installed = availability[prov.name];
85
+ const suffix = installed ? '' : ` ${dim(t('installer.llm_provider_not_installed'))}`;
86
+ return {
87
+ value: prov.name,
88
+ label: `${prov.label}${suffix}`,
89
+ };
90
+ });
91
+
92
+ const provider = await p.select({
93
+ message: t('installer.llm_provider_title'),
94
+ options,
95
+ initialValue: 'claude',
96
+ });
97
+
98
+ if (p.isCancel(provider)) {
99
+ p.cancel('Installation cancelled.');
100
+ process.exit(0);
101
+ }
102
+
103
+ return provider;
104
+ }
105
+
106
+ /**
107
+ * Step 4: Confirmation
62
108
  */
63
109
  export async function stepConfirmation(config) {
64
- const { projectName, projectType, language, selectedMCPs } = config;
110
+ const { projectName, projectType, language, llmProvider, selectedMCPs, selectedIDEs } = config;
65
111
 
66
112
  const langName = SUPPORTED_LANGUAGES.find(l => l.value === language)?.label || language;
67
- const ideNames = 'Claude Code (auto-configured)';
113
+ const providerNames = { claude: 'Claude (Anthropic)', gemini: 'Gemini (Google)', codex: 'Codex (OpenAI)', copilot: 'Copilot (GitHub)' };
114
+ const providerDisplay = providerNames[llmProvider] || llmProvider;
115
+ const ideNames = (selectedIDEs || ['claude-code'])
116
+ .map(id => IDE_CONFIGS[id]?.name || id)
117
+ .join(', ') + ' (auto-configured)';
68
118
  const mcpNames = selectedMCPs.length > 0
69
119
  ? `${selectedMCPs.join(', ')} (auto-installed)`
70
120
  : 'None';
@@ -74,6 +124,7 @@ export async function stepConfirmation(config) {
74
124
  showSummary({
75
125
  [t('installer.project_label')]: `${projectName} (${projectType === 'greenfield' ? 'Greenfield' : 'Brownfield'})`,
76
126
  [t('installer.language_label')]: langName,
127
+ [t('installer.llm_provider_label')]: providerDisplay,
77
128
  [t('installer.ides_label')]: ideNames,
78
129
  [t('installer.mcps_label')]: mcpNames,
79
130
  });