chati-dev 2.1.2 → 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 (95) hide show
  1. package/README.md +95 -24
  2. package/bin/chati.js +1 -1
  3. package/framework/agents/build/dev.md +1 -0
  4. package/framework/agents/deploy/devops.md +1 -0
  5. package/framework/agents/discover/brief.md +1 -0
  6. package/framework/agents/discover/brownfield-wu.md +1 -0
  7. package/framework/agents/discover/greenfield-wu.md +1 -0
  8. package/framework/agents/plan/architect.md +1 -0
  9. package/framework/agents/plan/detail.md +1 -0
  10. package/framework/agents/plan/phases.md +3 -2
  11. package/framework/agents/plan/tasks.md +1 -0
  12. package/framework/agents/plan/ux.md +1 -0
  13. package/framework/agents/quality/qa-implementation.md +1 -0
  14. package/framework/agents/quality/qa-planning.md +1 -0
  15. package/framework/config.yaml +21 -2
  16. package/framework/constitution.md +67 -2
  17. package/framework/context/governance.md +16 -3
  18. package/framework/context/root.md +3 -3
  19. package/framework/data/entity-registry.yaml +1 -1
  20. package/framework/domains/agents/brownfield-wu.yaml +4 -0
  21. package/framework/domains/agents/detail.yaml +1 -1
  22. package/framework/domains/agents/dev.yaml +4 -0
  23. package/framework/domains/agents/orchestrator.yaml +8 -0
  24. package/framework/domains/agents/phases.yaml +2 -2
  25. package/framework/domains/agents/tasks.yaml +2 -2
  26. package/framework/domains/constitution.yaml +30 -2
  27. package/framework/domains/global.yaml +20 -0
  28. package/framework/hooks/model-governance.js +17 -15
  29. package/framework/hooks/read-protection.js +1 -0
  30. package/framework/i18n/en.yaml +11 -4
  31. package/framework/i18n/es.yaml +11 -4
  32. package/framework/i18n/fr.yaml +11 -4
  33. package/framework/i18n/pt.yaml +11 -4
  34. package/framework/intelligence/context-engine.md +31 -2
  35. package/framework/intelligence/decision-engine.md +1 -1
  36. package/framework/intelligence/memory-layer.md +17 -0
  37. package/framework/orchestrator/chati.md +94 -14
  38. package/framework/patterns/elicitation-library.yaml +2 -2
  39. package/framework/schemas/config.schema.json +44 -0
  40. package/framework/schemas/session.schema.json +27 -0
  41. package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
  42. package/framework/tasks/brownfield-wu-migration-plan.md +4 -4
  43. package/framework/tasks/brownfield-wu-risk-assess.md +2 -2
  44. package/framework/tasks/detail-expand-prd.md +2 -2
  45. package/framework/tasks/greenfield-wu-report.md +1 -1
  46. package/framework/tasks/orchestrator-handoff.md +2 -2
  47. package/framework/tasks/orchestrator-resume.md +1 -1
  48. package/framework/tasks/qa-impl-performance-test.md +2 -2
  49. package/framework/tasks/qa-impl-sast-scan.md +6 -6
  50. package/framework/tasks/qa-planning-consolidate.md +1 -1
  51. package/framework/tasks/qa-planning-coverage-plan.md +9 -9
  52. package/framework/tasks/qa-planning-gate-define.md +6 -6
  53. package/framework/tasks/qa-planning-risk-matrix.md +1 -1
  54. package/framework/tasks/tasks-consolidate.md +5 -5
  55. package/package.json +5 -1
  56. package/scripts/changelog-generator.js +2 -2
  57. package/scripts/ide-sync.js +9 -4
  58. package/scripts/pr-review.js +5 -5
  59. package/scripts/semantic-lint.js +3 -3
  60. package/src/autonomy/build-loop.js +194 -0
  61. package/src/autonomy/build-state.js +269 -0
  62. package/src/autonomy/execution-profile.js +151 -0
  63. package/src/config/context-file-generator.js +203 -0
  64. package/src/config/ide-configs.js +6 -6
  65. package/src/gates/g2-qa-planning.js +4 -2
  66. package/src/gates/g4-qa-implementation.js +5 -2
  67. package/src/gates/gate-base.js +33 -1
  68. package/src/health/engine.js +245 -0
  69. package/src/installer/core.js +54 -16
  70. package/src/installer/templates.js +131 -2
  71. package/src/installer/validator.js +5 -3
  72. package/src/intelligence/file-tracker.js +117 -0
  73. package/src/intelligence/registry-manager.js +2 -2
  74. package/src/intelligence/timeline.js +144 -0
  75. package/src/memory/gotchas-auto-capture.js +253 -0
  76. package/src/preview/detector.js +1 -1
  77. package/src/terminal/adapters/claude-adapter.js +33 -0
  78. package/src/terminal/adapters/codex-adapter.js +31 -0
  79. package/src/terminal/adapters/copilot-adapter.js +28 -0
  80. package/src/terminal/adapters/gemini-adapter.js +28 -0
  81. package/src/terminal/adapters/index.js +8 -0
  82. package/src/terminal/cli-registry.js +190 -0
  83. package/src/terminal/prompt-builder.js +9 -20
  84. package/src/terminal/run-agent.js +2 -1
  85. package/src/terminal/spawner.js +44 -14
  86. package/src/terminal/wave-analyzer.js +143 -0
  87. package/src/utils/config-parser.js +97 -0
  88. package/src/wizard/feedback.js +1 -1
  89. package/src/wizard/i18n.js +11 -4
  90. package/src/wizard/index.js +22 -10
  91. package/src/wizard/questions.js +50 -2
  92. package/framework/manifest.json +0 -5
  93. package/framework/manifest.sig +0 -1
  94. /package/assets/{logo - co/314/201pia.png" → logo - c/303/263pia.png"} +0 -0
  95. /package/assets/{logo - co/314/201pia.svg" → logo - c/303/263pia.svg"} +0 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @fileoverview CLI Provider Registry for multi-CLI agent execution.
3
+ *
4
+ * Central registry of all supported CLI providers with their capabilities,
5
+ * command syntax, model maps, and feature support. This is the source of
6
+ * truth for multi-CLI governance (Constitution Article XIX).
7
+ */
8
+
9
+ import * as adapters from './adapters/index.js';
10
+ import { parseProviderConfig, parseAgentOverride } from '../utils/config-parser.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Provider Definitions
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * @typedef {object} ProviderConfig
18
+ * @property {string} name - Provider identifier
19
+ * @property {string} command - CLI command name
20
+ * @property {string[]} baseArgs - Default CLI arguments for non-interactive mode
21
+ * @property {string} modelFlag - CLI flag for model selection
22
+ * @property {boolean} stdinSupport - Whether prompts can be piped via stdin
23
+ * @property {boolean} hooksSupport - Whether the CLI supports hooks (event middleware)
24
+ * @property {boolean} mcpSupport - Whether the CLI supports MCP servers
25
+ * @property {string|null} contextFile - Project context file name (CLAUDE.md, GEMINI.md, etc.)
26
+ * @property {Record<string, string>} modelMap - Tier-to-model-id mapping
27
+ * @property {object} adapter - CLI-specific adapter module
28
+ */
29
+
30
+ /** @type {Record<string, ProviderConfig>} */
31
+ const PROVIDERS = {
32
+ claude: {
33
+ name: 'claude',
34
+ command: 'claude',
35
+ baseArgs: ['--print', '--dangerously-skip-permissions'],
36
+ modelFlag: '--model',
37
+ stdinSupport: true,
38
+ hooksSupport: true,
39
+ mcpSupport: true,
40
+ contextFile: 'CLAUDE.md',
41
+ modelMap: {
42
+ opus: 'claude-opus-4-6',
43
+ sonnet: 'claude-sonnet-4-5-20250929',
44
+ haiku: 'claude-haiku-4-5-20251001',
45
+ },
46
+ adapter: adapters.claude,
47
+ },
48
+ gemini: {
49
+ name: 'gemini',
50
+ command: 'gemini',
51
+ baseArgs: [],
52
+ modelFlag: '--model',
53
+ stdinSupport: true,
54
+ hooksSupport: false,
55
+ mcpSupport: true,
56
+ contextFile: 'GEMINI.md',
57
+ modelMap: {
58
+ pro: 'gemini-2.5-pro',
59
+ flash: 'gemini-2.5-flash',
60
+ },
61
+ adapter: adapters.gemini,
62
+ },
63
+ codex: {
64
+ name: 'codex',
65
+ command: 'codex',
66
+ baseArgs: ['exec'],
67
+ modelFlag: '-m',
68
+ stdinSupport: true,
69
+ hooksSupport: false,
70
+ mcpSupport: true,
71
+ contextFile: 'AGENTS.md',
72
+ modelMap: {
73
+ codex: 'gpt-5.3-codex',
74
+ mini: 'codex-mini-latest',
75
+ },
76
+ adapter: adapters.codex,
77
+ },
78
+ copilot: {
79
+ name: 'copilot',
80
+ command: 'copilot',
81
+ baseArgs: ['-p'],
82
+ modelFlag: '--model',
83
+ stdinSupport: true,
84
+ hooksSupport: false,
85
+ mcpSupport: true,
86
+ contextFile: null,
87
+ modelMap: {
88
+ 'claude-sonnet': 'claude-sonnet-4.5',
89
+ 'gpt-5': 'gpt-5',
90
+ },
91
+ adapter: adapters.copilot,
92
+ },
93
+ };
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Provider Resolution
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Get a provider configuration by name.
101
+ *
102
+ * @param {string} name - Provider name (claude, gemini, codex, copilot)
103
+ * @returns {ProviderConfig}
104
+ * @throws {Error} When provider is not found
105
+ */
106
+ export function getProvider(name) {
107
+ const provider = PROVIDERS[name];
108
+ if (!provider) {
109
+ throw new Error(`Unknown CLI provider: "${name}". Available: ${Object.keys(PROVIDERS).join(', ')}`);
110
+ }
111
+ return provider;
112
+ }
113
+
114
+ /**
115
+ * Get all registered providers.
116
+ *
117
+ * @returns {Record<string, ProviderConfig>}
118
+ */
119
+ export function getAllProviders() {
120
+ return { ...PROVIDERS };
121
+ }
122
+
123
+ /**
124
+ * Load enabled providers from project config.yaml.
125
+ *
126
+ * @param {string} projectDir - Project root directory
127
+ * @returns {{ primary: string, enabled: string[] }}
128
+ */
129
+ export function loadEnabledProviders(projectDir) {
130
+ const { primary, enabled } = parseProviderConfig(projectDir);
131
+ return { primary, enabled };
132
+ }
133
+
134
+ /**
135
+ * Resolve which provider should be used for a given agent.
136
+ * Priority: agent_overrides > agent default > primary provider.
137
+ *
138
+ * @param {string} agent - Agent name
139
+ * @param {string} projectDir - Project root directory
140
+ * @param {Record<string, {provider: string, model: string, tier: string}>} agentModels - Agent model assignments
141
+ * @returns {{ provider: string, model: string }}
142
+ */
143
+ export function resolveProviderForAgent(agent, projectDir, agentModels) {
144
+ const { primary, enabled } = loadEnabledProviders(projectDir);
145
+
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;
152
+ }
153
+ }
154
+
155
+ // Use agent's default assignment
156
+ const agentConfig = agentModels[agent];
157
+ if (agentConfig) {
158
+ const agentProvider = agentConfig.provider || primary;
159
+ if (enabled.includes(agentProvider)) {
160
+ return { provider: agentProvider, model: agentConfig.model };
161
+ }
162
+ }
163
+
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 };
168
+ }
169
+
170
+ /**
171
+ * Check if a provider CLI is available on the system.
172
+ *
173
+ * @param {string} name - Provider name
174
+ * @returns {Promise<boolean>}
175
+ */
176
+ export async function isProviderAvailable(name) {
177
+ const provider = PROVIDERS[name];
178
+ if (!provider) return false;
179
+
180
+ const { execFileSync } = await import('child_process');
181
+ try {
182
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
183
+ execFileSync(cmd, [provider.command], { stdio: 'ignore' });
184
+ return true;
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ export { PROVIDERS };
@@ -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: 'sonnet',
22
- 'greenfield-wu': 'haiku',
23
- 'brownfield-wu': 'opus',
24
- brief: 'sonnet',
25
- detail: 'opus',
26
- architect: 'opus',
27
- ux: 'sonnet',
28
- phases: 'sonnet',
29
- tasks: 'sonnet',
30
- 'qa-planning': 'opus',
31
- 'qa-implementation': 'opus',
32
- dev: 'opus',
33
- devops: '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).
@@ -116,11 +102,13 @@ export function buildAgentPrompt(config) {
116
102
  sections.push(buildOutputInstructions());
117
103
 
118
104
  const prompt = sections.join('\n\n---\n\n');
119
- const model = AGENT_MODELS[config.agent] || 'sonnet';
105
+ const assignment = AGENT_MODELS[config.agent] || { provider: 'claude', model: 'sonnet', tier: 'sonnet' };
106
+ const model = assignment.model || assignment;
120
107
 
121
108
  return {
122
109
  prompt,
123
110
  model,
111
+ provider: assignment.provider || 'claude',
124
112
  metadata: {
125
113
  agent: config.agent,
126
114
  layers: prismResult.layerCount || 0,
@@ -266,7 +254,8 @@ function buildSessionSection(config) {
266
254
  `- **User Level**: ${state.user_level || 'auto'}`,
267
255
  `- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
268
256
  `- **Your Agent**: ${config.agent}`,
269
- `- **Your Model**: ${AGENT_MODELS[config.agent] || 'sonnet'}`,
257
+ `- **Your Model**: ${(AGENT_MODELS[config.agent]?.model) || 'sonnet'}`,
258
+ `- **Your Provider**: ${(AGENT_MODELS[config.agent]?.provider) || 'claude'}`,
270
259
  ];
271
260
 
272
261
  return lines.join('\n');
@@ -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(() => {
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { spawn } from 'child_process';
11
11
  import { validateWriteScopes, buildIsolationEnv } from './isolation.js';
12
+ import { getProvider } from './cli-registry.js';
12
13
 
13
14
  // ---------------------------------------------------------------------------
14
15
  // Helpers
@@ -42,7 +43,8 @@ export function _resetCounter() {
42
43
  * @typedef {object} SpawnConfig
43
44
  * @property {string} agent - Agent name (e.g. "architect")
44
45
  * @property {string} taskId - Task identifier
45
- * @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)
46
48
  * @property {string} [prompt] - Full prompt string (from prompt-builder, piped via stdin)
47
49
  * @property {object} [contextPayload] - Context to inject via env var
48
50
  * @property {string[]} [writeScope] - Override write scope
@@ -103,17 +105,29 @@ export function buildSpawnCommand(config) {
103
105
  }
104
106
  }
105
107
 
106
- // Build CLI argsprompt is piped via stdin, NOT as a CLI argument
107
- const command = 'claude';
108
- const args = ['--print', '--dangerously-skip-permissions'];
109
-
110
- if (config.model) {
111
- args.push('--model', config.model);
108
+ // Resolve CLI providerdefaults to claude for backwards compatibility
109
+ const providerName = config.provider || 'claude';
110
+ let command, args, prompt;
111
+
112
+ try {
113
+ const provider = getProvider(providerName);
114
+ const adapterResult = provider.adapter.buildCommand(config, provider);
115
+ command = adapterResult.command;
116
+ args = adapterResult.args;
117
+ prompt = adapterResult.stdinPrompt;
118
+ } catch (err) {
119
+ // Fallback to claude if provider resolution fails (backwards compatibility)
120
+ console.error(`[chati] Provider "${providerName}" resolution failed: ${err.message}. Falling back to claude.`);
121
+ command = 'claude';
122
+ args = ['--print', '--dangerously-skip-permissions'];
123
+ if (config.model) {
124
+ const claudeProvider = getProvider('claude');
125
+ const resolvedModel = claudeProvider.modelMap[config.model] || config.model;
126
+ args.push('--model', resolvedModel);
127
+ }
128
+ prompt = config.prompt || null;
112
129
  }
113
130
 
114
- // Prompt is returned separately for stdin piping (avoids ARG_MAX limits)
115
- const prompt = config.prompt || null;
116
-
117
131
  return { command, args, env, terminalId, prompt };
118
132
  }
119
133
 
@@ -147,7 +161,7 @@ export function spawnTerminal(config) {
147
161
  process: child,
148
162
  agent: config.agent,
149
163
  taskId: config.taskId,
150
- model: config.model || 'sonnet',
164
+ model: config.model || 'unknown',
151
165
  startedAt: new Date().toISOString(),
152
166
  status: 'running',
153
167
  exitCode: null,
@@ -156,15 +170,20 @@ export function spawnTerminal(config) {
156
170
  timeout,
157
171
  };
158
172
 
159
- // Capture output
173
+ // Capture output (capped at ~10MB to prevent unbounded memory growth)
174
+ const MAX_BUFFER_CHUNKS = 10_000;
160
175
  if (child.stdout) {
161
176
  child.stdout.on('data', (chunk) => {
162
- handle.stdout.push(chunk.toString());
177
+ if (handle.stdout.length < MAX_BUFFER_CHUNKS) {
178
+ handle.stdout.push(chunk.toString());
179
+ }
163
180
  });
164
181
  }
165
182
  if (child.stderr) {
166
183
  child.stderr.on('data', (chunk) => {
167
- handle.stderr.push(chunk.toString());
184
+ if (handle.stderr.length < MAX_BUFFER_CHUNKS) {
185
+ handle.stderr.push(chunk.toString());
186
+ }
168
187
  });
169
188
  }
170
189
 
@@ -179,6 +198,17 @@ export function spawnTerminal(config) {
179
198
  handle.stderr.push(`spawn error: ${err.message}`);
180
199
  });
181
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
+
182
212
  return handle;
183
213
  }
184
214
 
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @fileoverview Wave analyzer for task parallelization.
3
+ *
4
+ * Analyzes task dependencies to group them into waves —
5
+ * sets of tasks that can execute in parallel because they
6
+ * have no inter-dependencies.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * @typedef {object} TaskNode
15
+ * @property {string} id - Task identifier
16
+ * @property {string[]} dependencies - IDs of tasks this depends on
17
+ * @property {string[]} [writeScope] - Files/dirs this task writes to
18
+ */
19
+
20
+ /**
21
+ * @typedef {object} Wave
22
+ * @property {number} index - Wave number (0-based)
23
+ * @property {string[]} taskIds - Task IDs in this wave
24
+ */
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Wave Analysis
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * Build a dependency graph and compute waves using topological sorting.
32
+ *
33
+ * @param {TaskNode[]} tasks - Array of tasks with dependencies
34
+ * @returns {Wave[]}
35
+ * @throws {Error} When circular dependencies are detected
36
+ */
37
+ export function analyzeWaves(tasks) {
38
+ if (!tasks || tasks.length === 0) return [];
39
+
40
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
41
+ const inDegree = new Map();
42
+ const adjList = new Map();
43
+
44
+ // Initialize
45
+ for (const task of tasks) {
46
+ inDegree.set(task.id, 0);
47
+ adjList.set(task.id, []);
48
+ }
49
+
50
+ // Build adjacency and in-degree
51
+ for (const task of tasks) {
52
+ for (const dep of task.dependencies || []) {
53
+ if (taskMap.has(dep)) {
54
+ adjList.get(dep).push(task.id);
55
+ inDegree.set(task.id, (inDegree.get(task.id) || 0) + 1);
56
+ }
57
+ }
58
+ }
59
+
60
+ // Kahn's algorithm with wave grouping
61
+ const waves = [];
62
+ const remaining = new Set(tasks.map((t) => t.id));
63
+
64
+ while (remaining.size > 0) {
65
+ // Find all tasks with no pending dependencies (in-degree 0)
66
+ const wave = [];
67
+ for (const id of remaining) {
68
+ if ((inDegree.get(id) || 0) === 0) {
69
+ wave.push(id);
70
+ }
71
+ }
72
+
73
+ if (wave.length === 0) {
74
+ const circular = [...remaining].join(', ');
75
+ throw new Error(`Circular dependency detected among tasks: ${circular}`);
76
+ }
77
+
78
+ waves.push({ index: waves.length, taskIds: wave });
79
+
80
+ // Remove wave tasks and update in-degrees
81
+ for (const id of wave) {
82
+ remaining.delete(id);
83
+ for (const dependent of adjList.get(id) || []) {
84
+ inDegree.set(dependent, (inDegree.get(dependent) || 0) - 1);
85
+ }
86
+ }
87
+ }
88
+
89
+ return waves;
90
+ }
91
+
92
+ /**
93
+ * Validate that tasks within a wave don't have write scope conflicts.
94
+ *
95
+ * @param {TaskNode[]} tasks - All tasks
96
+ * @param {Wave} wave - Wave to validate
97
+ * @returns {{ valid: boolean, conflicts: { taskA: string, taskB: string, path: string }[] }}
98
+ */
99
+ export function validateWaveScopes(tasks, wave) {
100
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
101
+ const conflicts = [];
102
+
103
+ const waveTasks = wave.taskIds.map((id) => taskMap.get(id)).filter(Boolean);
104
+
105
+ for (let i = 0; i < waveTasks.length; i++) {
106
+ for (let j = i + 1; j < waveTasks.length; j++) {
107
+ const scopeA = waveTasks[i].writeScope || [];
108
+ const scopeB = waveTasks[j].writeScope || [];
109
+
110
+ for (const pathA of scopeA) {
111
+ for (const pathB of scopeB) {
112
+ if (pathA === pathB || pathA.startsWith(pathB + '/') || pathB.startsWith(pathA + '/')) {
113
+ conflicts.push({
114
+ taskA: waveTasks[i].id,
115
+ taskB: waveTasks[j].id,
116
+ path: pathA,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ return { valid: conflicts.length === 0, conflicts };
125
+ }
126
+
127
+ /**
128
+ * Get a summary of the wave analysis.
129
+ *
130
+ * @param {Wave[]} waves
131
+ * @returns {{ totalWaves: number, totalTasks: number, maxParallel: number, sequential: boolean }}
132
+ */
133
+ export function getWaveSummary(waves) {
134
+ const totalTasks = waves.reduce((sum, w) => sum + w.taskIds.length, 0);
135
+ const maxParallel = Math.max(...waves.map((w) => w.taskIds.length), 0);
136
+
137
+ return {
138
+ totalWaves: waves.length,
139
+ totalTasks,
140
+ maxParallel,
141
+ sequential: waves.every((w) => w.taskIds.length === 1),
142
+ };
143
+ }
@@ -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',