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.
- package/README.md +96 -24
- package/assets/logo2.png +0 -0
- package/assets/social-preview.png +0 -0
- package/bin/chati.js +1 -1
- package/framework/agents/plan/phases.md +2 -2
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +4 -4
- package/framework/context/governance.md +16 -3
- package/framework/context/root.md +3 -3
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/detail.yaml +1 -1
- package/framework/domains/agents/phases.yaml +2 -2
- package/framework/domains/agents/tasks.yaml +2 -2
- package/framework/domains/constitution.yaml +2 -2
- package/framework/hooks/read-protection.js +1 -0
- package/framework/i18n/en.yaml +11 -4
- package/framework/i18n/es.yaml +11 -4
- package/framework/i18n/fr.yaml +11 -4
- package/framework/i18n/pt.yaml +11 -4
- package/framework/intelligence/context-engine.md +4 -4
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/patterns/elicitation-library.yaml +2 -2
- package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
- package/framework/tasks/brownfield-wu-migration-plan.md +4 -4
- package/framework/tasks/brownfield-wu-risk-assess.md +2 -2
- package/framework/tasks/detail-expand-prd.md +2 -2
- package/framework/tasks/greenfield-wu-report.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +2 -2
- package/framework/tasks/orchestrator-resume.md +1 -1
- package/framework/tasks/qa-impl-performance-test.md +2 -2
- package/framework/tasks/qa-impl-sast-scan.md +6 -6
- package/framework/tasks/qa-planning-consolidate.md +1 -1
- package/framework/tasks/qa-planning-coverage-plan.md +9 -9
- package/framework/tasks/qa-planning-gate-define.md +6 -6
- package/framework/tasks/qa-planning-risk-matrix.md +1 -1
- package/framework/tasks/tasks-consolidate.md +5 -5
- package/package.json +1 -1
- package/scripts/changelog-generator.js +2 -2
- package/scripts/ide-sync.js +9 -4
- package/scripts/pr-review.js +5 -5
- package/scripts/semantic-lint.js +3 -3
- package/src/autonomy/build-state.js +1 -1
- package/src/autonomy/execution-profile.js +1 -1
- package/src/config/context-file-generator.js +31 -34
- package/src/config/ide-configs.js +15 -6
- package/src/dashboard/layout.js +3 -1
- package/src/gates/g2-qa-planning.js +1 -1
- package/src/gates/g4-qa-implementation.js +1 -1
- package/src/health/engine.js +7 -12
- package/src/installer/core.js +69 -34
- package/src/installer/templates.js +168 -2
- package/src/installer/validator.js +5 -3
- package/src/intelligence/registry-manager.js +2 -2
- package/src/preview/detector.js +1 -1
- package/src/terminal/adapters/claude-adapter.js +3 -13
- package/src/terminal/adapters/codex-adapter.js +3 -13
- package/src/terminal/adapters/copilot-adapter.js +3 -13
- package/src/terminal/adapters/gemini-adapter.js +3 -17
- package/src/terminal/cli-registry.js +20 -48
- package/src/terminal/index.js +1 -0
- package/src/terminal/prompt-builder.js +3 -18
- package/src/terminal/run-agent.js +2 -1
- package/src/terminal/spawner.js +70 -8
- package/src/terminal/wave-analyzer.js +1 -1
- package/src/utils/config-parser.js +97 -0
- package/src/wizard/feedback.js +1 -1
- package/src/wizard/i18n.js +11 -4
- package/src/wizard/index.js +45 -15
- package/src/wizard/questions.js +54 -3
- 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: [
|
|
51
|
+
baseArgs: [],
|
|
53
52
|
modelFlag: '--model',
|
|
54
53
|
stdinSupport: true,
|
|
55
|
-
hooksSupport:
|
|
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: '
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
175
|
-
if (
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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 {
|
|
180
|
+
const { execFileSync } = await import('child_process');
|
|
210
181
|
try {
|
|
211
|
-
|
|
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;
|
package/src/terminal/index.js
CHANGED
|
@@ -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
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
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).
|
package/src/terminal/spawner.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 || '
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/wizard/feedback.js
CHANGED
|
@@ -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(
|
|
63
|
+
const paddedLabel = label.padEnd(16);
|
|
64
64
|
console.log(` ${dim(paddedLabel)} ${value}`);
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/wizard/i18n.js
CHANGED
|
@@ -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 (
|
|
34
|
+
constitution: 'Constitution (19 Articles + Preamble)',
|
|
28
35
|
session_mgmt: 'Session management system',
|
|
29
|
-
quality_gates: 'Quality gates (
|
|
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-
|
|
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:
|
|
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',
|
package/src/wizard/index.js
CHANGED
|
@@ -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
|
-
//
|
|
39
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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')} (
|
|
97
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/wizard/questions.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
});
|