chati-dev 3.0.0 → 3.0.1

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 (65) hide show
  1. package/README.md +95 -24
  2. package/bin/chati.js +1 -1
  3. package/framework/agents/plan/phases.md +2 -2
  4. package/framework/config.yaml +2 -2
  5. package/framework/constitution.md +4 -4
  6. package/framework/context/governance.md +16 -3
  7. package/framework/context/root.md +3 -3
  8. package/framework/data/entity-registry.yaml +1 -1
  9. package/framework/domains/agents/detail.yaml +1 -1
  10. package/framework/domains/agents/phases.yaml +2 -2
  11. package/framework/domains/agents/tasks.yaml +2 -2
  12. package/framework/domains/constitution.yaml +2 -2
  13. package/framework/hooks/read-protection.js +1 -0
  14. package/framework/i18n/en.yaml +11 -4
  15. package/framework/i18n/es.yaml +11 -4
  16. package/framework/i18n/fr.yaml +11 -4
  17. package/framework/i18n/pt.yaml +11 -4
  18. package/framework/intelligence/context-engine.md +2 -2
  19. package/framework/intelligence/decision-engine.md +1 -1
  20. package/framework/patterns/elicitation-library.yaml +2 -2
  21. package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
  22. package/framework/tasks/brownfield-wu-migration-plan.md +4 -4
  23. package/framework/tasks/brownfield-wu-risk-assess.md +2 -2
  24. package/framework/tasks/detail-expand-prd.md +2 -2
  25. package/framework/tasks/greenfield-wu-report.md +1 -1
  26. package/framework/tasks/orchestrator-handoff.md +2 -2
  27. package/framework/tasks/orchestrator-resume.md +1 -1
  28. package/framework/tasks/qa-impl-performance-test.md +2 -2
  29. package/framework/tasks/qa-impl-sast-scan.md +6 -6
  30. package/framework/tasks/qa-planning-consolidate.md +1 -1
  31. package/framework/tasks/qa-planning-coverage-plan.md +9 -9
  32. package/framework/tasks/qa-planning-gate-define.md +6 -6
  33. package/framework/tasks/qa-planning-risk-matrix.md +1 -1
  34. package/framework/tasks/tasks-consolidate.md +5 -5
  35. package/package.json +1 -1
  36. package/scripts/changelog-generator.js +2 -2
  37. package/scripts/ide-sync.js +9 -4
  38. package/scripts/pr-review.js +5 -5
  39. package/scripts/semantic-lint.js +3 -3
  40. package/src/autonomy/build-state.js +1 -1
  41. package/src/autonomy/execution-profile.js +1 -1
  42. package/src/config/context-file-generator.js +20 -26
  43. package/src/config/ide-configs.js +6 -6
  44. package/src/gates/g2-qa-planning.js +1 -1
  45. package/src/gates/g4-qa-implementation.js +1 -1
  46. package/src/health/engine.js +7 -12
  47. package/src/installer/core.js +54 -16
  48. package/src/installer/templates.js +131 -2
  49. package/src/installer/validator.js +5 -3
  50. package/src/intelligence/registry-manager.js +2 -2
  51. package/src/preview/detector.js +1 -1
  52. package/src/terminal/adapters/claude-adapter.js +3 -13
  53. package/src/terminal/adapters/codex-adapter.js +3 -13
  54. package/src/terminal/adapters/copilot-adapter.js +3 -13
  55. package/src/terminal/adapters/gemini-adapter.js +3 -17
  56. package/src/terminal/cli-registry.js +20 -48
  57. package/src/terminal/prompt-builder.js +4 -18
  58. package/src/terminal/run-agent.js +2 -1
  59. package/src/terminal/spawner.js +27 -7
  60. package/src/terminal/wave-analyzer.js +1 -1
  61. package/src/utils/config-parser.js +97 -0
  62. package/src/wizard/feedback.js +1 -1
  63. package/src/wizard/i18n.js +11 -4
  64. package/src/wizard/index.js +22 -10
  65. package/src/wizard/questions.js +50 -2
@@ -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;
@@ -14,24 +14,10 @@ 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 DEFAULT_AGENT_MODELS from model-governance (safe named export,
18
+ // does not trigger main() which is guarded by fileURLToPath check).
19
+ import { DEFAULT_AGENT_MODELS } from '../../framework/hooks/model-governance.js';
20
+ const AGENT_MODELS = DEFAULT_AGENT_MODELS;
35
21
 
36
22
  /**
37
23
  * 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(() => {
@@ -43,7 +43,8 @@ export function _resetCounter() {
43
43
  * @typedef {object} SpawnConfig
44
44
  * @property {string} agent - Agent name (e.g. "architect")
45
45
  * @property {string} taskId - Task identifier
46
- * @property {string} [model] - LLM model (haiku/sonnet/opus)
46
+ * @property {string} [model] - LLM model tier name (e.g. opus, pro, codex, claude-sonnet)
47
+ * @property {string} [provider] - CLI provider name (claude, gemini, codex, copilot)
47
48
  * @property {string} [prompt] - Full prompt string (from prompt-builder, piped via stdin)
48
49
  * @property {object} [contextPayload] - Context to inject via env var
49
50
  * @property {string[]} [writeScope] - Override write scope
@@ -114,12 +115,15 @@ export function buildSpawnCommand(config) {
114
115
  command = adapterResult.command;
115
116
  args = adapterResult.args;
116
117
  prompt = adapterResult.stdinPrompt;
117
- } catch {
118
+ } catch (err) {
118
119
  // Fallback to claude if provider resolution fails (backwards compatibility)
120
+ console.error(`[chati] Provider "${providerName}" resolution failed: ${err.message}. Falling back to claude.`);
119
121
  command = 'claude';
120
122
  args = ['--print', '--dangerously-skip-permissions'];
121
123
  if (config.model) {
122
- args.push('--model', config.model);
124
+ const claudeProvider = getProvider('claude');
125
+ const resolvedModel = claudeProvider.modelMap[config.model] || config.model;
126
+ args.push('--model', resolvedModel);
123
127
  }
124
128
  prompt = config.prompt || null;
125
129
  }
@@ -157,7 +161,7 @@ export function spawnTerminal(config) {
157
161
  process: child,
158
162
  agent: config.agent,
159
163
  taskId: config.taskId,
160
- model: config.model || 'sonnet',
164
+ model: config.model || 'unknown',
161
165
  startedAt: new Date().toISOString(),
162
166
  status: 'running',
163
167
  exitCode: null,
@@ -166,15 +170,20 @@ export function spawnTerminal(config) {
166
170
  timeout,
167
171
  };
168
172
 
169
- // Capture output
173
+ // Capture output (capped at ~10MB to prevent unbounded memory growth)
174
+ const MAX_BUFFER_CHUNKS = 10_000;
170
175
  if (child.stdout) {
171
176
  child.stdout.on('data', (chunk) => {
172
- handle.stdout.push(chunk.toString());
177
+ if (handle.stdout.length < MAX_BUFFER_CHUNKS) {
178
+ handle.stdout.push(chunk.toString());
179
+ }
173
180
  });
174
181
  }
175
182
  if (child.stderr) {
176
183
  child.stderr.on('data', (chunk) => {
177
- handle.stderr.push(chunk.toString());
184
+ if (handle.stderr.length < MAX_BUFFER_CHUNKS) {
185
+ handle.stderr.push(chunk.toString());
186
+ }
178
187
  });
179
188
  }
180
189
 
@@ -189,6 +198,17 @@ export function spawnTerminal(config) {
189
198
  handle.stderr.push(`spawn error: ${err.message}`);
190
199
  });
191
200
 
201
+ // Enforce timeout — kill process if it exceeds max execution time
202
+ const timeoutTimer = setTimeout(() => {
203
+ if (handle.status === 'running') {
204
+ handle.stderr.push(`timeout: process exceeded ${timeout}ms limit`);
205
+ killTerminal(handle);
206
+ }
207
+ }, timeout);
208
+
209
+ // Clear timer when process exits normally
210
+ child.on('exit', () => clearTimeout(timeoutTimer));
211
+
192
212
  return handle;
193
213
  }
194
214
 
@@ -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,7 +3,7 @@ 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';
@@ -35,16 +35,20 @@ export async function runWizard(targetDir, options = {}) {
35
35
  // Step 2: Project Type
36
36
  const projectType = options.projectType || await stepProjectType(targetDir);
37
37
 
38
+ // Step 3: LLM Provider
39
+ const llmProvider = options.llmProvider || await stepLlmProvider();
40
+
38
41
  // Auto-configured: Claude Code + default MCPs
39
42
  const selectedIDEs = options.ides || ['claude-code'];
40
43
  const selectedMCPs = options.mcps || DEFAULT_MCPS;
41
44
 
42
- // Step 3: Confirmation
45
+ // Step 4: Confirmation
43
46
  const projectName = basename(targetDir);
44
47
  const config = {
45
48
  projectName,
46
49
  projectType,
47
50
  language,
51
+ llmProvider,
48
52
  selectedIDEs,
49
53
  selectedMCPs,
50
54
  targetDir,
@@ -53,7 +57,7 @@ export async function runWizard(targetDir, options = {}) {
53
57
 
54
58
  await stepConfirmation(config);
55
59
 
56
- // Step 4: Installation + Validation
60
+ // Step 5: Installation + Validation
57
61
  console.log();
58
62
  const installSpinner = createSpinner(t('installer.installing'));
59
63
  installSpinner.start();
@@ -80,14 +84,21 @@ export async function runWizard(targetDir, options = {}) {
80
84
  const validation = await validateInstallation(targetDir);
81
85
  validateSpinner.stop();
82
86
 
83
- showValidation(t('installer.agents_valid'));
87
+ // Show validation results based on actual checks
88
+ if (validation.agents?.pass) showValidation(t('installer.agents_valid'));
89
+ if (validation.constitution?.pass) showValidation(t('installer.constitution_ok'));
90
+ if (validation.intelligence?.pass) showValidation(t('installer.intelligence_valid'));
91
+ if (validation.registry?.pass) showValidation(t('installer.registry_valid'));
92
+ if (validation.memories?.pass) showValidation(t('installer.memories_valid'));
93
+ if (validation.session?.pass) showValidation(t('installer.session_ok'));
84
94
  showValidation(t('installer.handoff_ok'));
85
95
  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'));
96
+
97
+ // Warn if any checks failed
98
+ if (validation.passed < validation.total) {
99
+ const failed = validation.total - validation.passed;
100
+ p.log.warn(`${failed} validation check(s) did not pass. Run 'npx chati-dev health' for details.`);
101
+ }
91
102
 
92
103
  console.log();
93
104
  p.outro(t('installer.success'));
@@ -101,7 +112,8 @@ export async function runWizard(targetDir, options = {}) {
101
112
  return { success: true, config, validation };
102
113
  } catch (err) {
103
114
  installSpinner.stop();
104
- p.cancel(`Installation failed: ${err.message}`);
115
+ const phase = err.message?.includes('validat') ? 'Validation' : 'Installation';
116
+ p.cancel(`${phase} failed: ${err.message}`);
105
117
  return { success: false, error: err.message };
106
118
  }
107
119
  }
@@ -3,6 +3,7 @@ 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';
6
7
 
7
8
  /**
8
9
  * Step 1: Language Selection (always in English)
@@ -58,12 +59,58 @@ export async function stepProjectType(targetDir) {
58
59
  }
59
60
 
60
61
  /**
61
- * Step 3: Confirmation
62
+ * Step 3: LLM Provider Selection
63
+ */
64
+ export async function stepLlmProvider() {
65
+ const providers = [
66
+ { name: 'claude', label: t('installer.llm_provider_claude') },
67
+ { name: 'gemini', label: t('installer.llm_provider_gemini') },
68
+ { name: 'codex', label: t('installer.llm_provider_codex') },
69
+ { name: 'copilot', label: t('installer.llm_provider_copilot') },
70
+ ];
71
+
72
+ // Check availability of each provider
73
+ const availability = {};
74
+ for (const prov of providers) {
75
+ try {
76
+ availability[prov.name] = await isProviderAvailable(prov.name);
77
+ } catch {
78
+ availability[prov.name] = prov.name === 'claude'; // claude always available
79
+ }
80
+ }
81
+
82
+ const options = providers.map(prov => {
83
+ const installed = availability[prov.name];
84
+ const suffix = installed ? '' : ` ${dim(t('installer.llm_provider_not_installed'))}`;
85
+ return {
86
+ value: prov.name,
87
+ label: `${prov.label}${suffix}`,
88
+ };
89
+ });
90
+
91
+ const provider = await p.select({
92
+ message: t('installer.llm_provider_title'),
93
+ options,
94
+ initialValue: 'claude',
95
+ });
96
+
97
+ if (p.isCancel(provider)) {
98
+ p.cancel('Installation cancelled.');
99
+ process.exit(0);
100
+ }
101
+
102
+ return provider;
103
+ }
104
+
105
+ /**
106
+ * Step 4: Confirmation
62
107
  */
63
108
  export async function stepConfirmation(config) {
64
- const { projectName, projectType, language, selectedMCPs } = config;
109
+ const { projectName, projectType, language, llmProvider, selectedMCPs } = config;
65
110
 
66
111
  const langName = SUPPORTED_LANGUAGES.find(l => l.value === language)?.label || language;
112
+ const providerNames = { claude: 'Claude (Anthropic)', gemini: 'Gemini (Google)', codex: 'Codex (OpenAI)', copilot: 'Copilot (GitHub)' };
113
+ const providerDisplay = providerNames[llmProvider] || llmProvider;
67
114
  const ideNames = 'Claude Code (auto-configured)';
68
115
  const mcpNames = selectedMCPs.length > 0
69
116
  ? `${selectedMCPs.join(', ')} (auto-installed)`
@@ -74,6 +121,7 @@ export async function stepConfirmation(config) {
74
121
  showSummary({
75
122
  [t('installer.project_label')]: `${projectName} (${projectType === 'greenfield' ? 'Greenfield' : 'Brownfield'})`,
76
123
  [t('installer.language_label')]: langName,
124
+ [t('installer.llm_provider_label')]: providerDisplay,
77
125
  [t('installer.ides_label')]: ideNames,
78
126
  [t('installer.mcps_label')]: mcpNames,
79
127
  });