chati-dev 4.5.1 → 4.5.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.
@@ -1,8 +1,8 @@
1
1
  # chati.dev Configuration
2
- version: "4.5.1"
2
+ version: "4.5.2"
3
3
  installed_at: "2026-02-07T10:00:00Z"
4
4
  updated_at: "2026-06-11T00:00:00Z"
5
- installer_version: "4.5.1"
5
+ installer_version: "4.5.2"
6
6
  project_type: greenfield
7
7
  language: en
8
8
  ides: [claude-code]
@@ -1,7 +1,7 @@
1
1
  # Chati.dev System Context
2
2
 
3
3
  ## Framework
4
- - **Version**: 4.5.1
4
+ - **Version**: 4.5.2
5
5
  - **Agents**: Specialized discovery and planning agents feeding provider-neutral RAIL execution and a sealed release lane
6
6
  - **Constitution**: 25 Articles + Preamble
7
7
  - **Quality**: 5 pipeline gates + 3-tier verdicts + Fault Vector Protocol (Article XXII)
@@ -22,8 +22,14 @@ installer:
22
22
  llm_provider_not_installed: "(CLI not detected)"
23
23
  llm_provider_label: "AI Provider"
24
24
  ide_selection_title: "Select your IDEs / CLIs (multiple allowed):"
25
- primary_provider_title: "Which is your primary CLI provider?"
26
- primary_provider_label: "Primary Provider"
25
+ installation_mode_title: "Select the installation mode"
26
+ installation_mode_open: "Open"
27
+ installation_mode_open_hint: "Public framework without mandatory company integrations"
28
+ installation_mode_internal: "Internal"
29
+ installation_mode_internal_hint: "Requires an authorized license and enables ClickUp tracking plus internal policies"
30
+ installation_mode_label: "Installation mode"
31
+ model_routing_label: "Model routing"
32
+ model_routing_automatic: "Automatic per task across all enabled models"
27
33
  providers_label: "Providers"
28
34
  quick_start_switch_hint: "Switch CLIs anytime — your session continues from where you left off"
29
35
  created_overlays: "Created provider overlay directories"
@@ -22,8 +22,14 @@ installer:
22
22
  llm_provider_not_installed: "(CLI no detectada)"
23
23
  llm_provider_label: "Proveedor de IA"
24
24
  ide_selection_title: "Seleccione sus IDEs / CLIs (multiples permitidos):"
25
- primary_provider_title: "Cual es su proveedor CLI principal?"
26
- primary_provider_label: "Proveedor Principal"
25
+ installation_mode_title: "Seleccione el modo de instalacion"
26
+ installation_mode_open: "Open"
27
+ installation_mode_open_hint: "Framework publico sin integraciones corporativas obligatorias"
28
+ installation_mode_internal: "Internal"
29
+ installation_mode_internal_hint: "Requiere una licencia autorizada y activa el tracking en ClickUp y las politicas internas"
30
+ installation_mode_label: "Modo de instalacion"
31
+ model_routing_label: "Enrutamiento de modelos"
32
+ model_routing_automatic: "Automatico por tarea entre todos los modelos habilitados"
27
33
  providers_label: "Proveedores"
28
34
  quick_start_switch_hint: "Cambie de CLI en cualquier momento — su sesion continua desde donde quedo"
29
35
  created_overlays: "Creados directorios de overlay de proveedores"
@@ -22,8 +22,14 @@ installer:
22
22
  llm_provider_not_installed: "(CLI non detecte)"
23
23
  llm_provider_label: "Fournisseur IA"
24
24
  ide_selection_title: "Selectionnez vos IDEs / CLIs (multiples autorises):"
25
- primary_provider_title: "Quel est votre fournisseur CLI principal?"
26
- primary_provider_label: "Fournisseur Principal"
25
+ installation_mode_title: "Selectionnez le mode d'installation"
26
+ installation_mode_open: "Open"
27
+ installation_mode_open_hint: "Framework public sans integrations d'entreprise obligatoires"
28
+ installation_mode_internal: "Internal"
29
+ installation_mode_internal_hint: "Necessite une licence autorisee et active le suivi ClickUp et les politiques internes"
30
+ installation_mode_label: "Mode d'installation"
31
+ model_routing_label: "Routage des modeles"
32
+ model_routing_automatic: "Automatique par tache entre tous les modeles actives"
27
33
  providers_label: "Fournisseurs"
28
34
  quick_start_switch_hint: "Changez de CLI a tout moment — votre session continue la ou vous l'avez laissee"
29
35
  created_overlays: "Crees les repertoires d'overlay des fournisseurs"
@@ -22,8 +22,14 @@ installer:
22
22
  llm_provider_not_installed: "(CLI nao detectada)"
23
23
  llm_provider_label: "Provedor de IA"
24
24
  ide_selection_title: "Selecione suas IDEs / CLIs (multiplos permitidos):"
25
- primary_provider_title: "Qual e seu provedor CLI principal?"
26
- primary_provider_label: "Provedor Principal"
25
+ installation_mode_title: "Selecione o modo de instalacao"
26
+ installation_mode_open: "Open"
27
+ installation_mode_open_hint: "Framework publico sem integracoes corporativas obrigatorias"
28
+ installation_mode_internal: "Internal"
29
+ installation_mode_internal_hint: "Exige uma licenca autorizada e ativa o tracking no ClickUp e as politicas internas"
30
+ installation_mode_label: "Modo de instalacao"
31
+ model_routing_label: "Roteamento de modelos"
32
+ model_routing_automatic: "Automatico por task entre todos os modelos habilitados"
27
33
  providers_label: "Provedores"
28
34
  quick_start_switch_hint: "Troque de CLI a qualquer momento — sua sessao continua de onde parou"
29
35
  created_overlays: "Criados diretorios de overlay de provedores"
@@ -1,5 +1,5 @@
1
1
  {
2
- "createdAt": "2026-08-26T20:08:59.239Z",
2
+ "createdAt": "2026-08-27T07:27:47.925Z",
3
3
  "files": {
4
4
  "agents/build/dev.md": {
5
5
  "hash": "5c8da2eee3357bbe84a6c468c76fe60a0c77357575503c3797aa94128282c565",
@@ -97,7 +97,7 @@
97
97
  "type": "md"
98
98
  },
99
99
  "config.yaml": {
100
- "hash": "46011fbc84ddcb0d5aa5c3c3a303bcb649983568000d256574afaaa4efe585ab",
100
+ "hash": "f7c77483aeaa44f37d8492d85a376304cb24ec28d91dbed3155f41faa627b7ad",
101
101
  "size": 2841,
102
102
  "type": "yaml"
103
103
  },
@@ -122,7 +122,7 @@
122
122
  "type": "md"
123
123
  },
124
124
  "context/root.md": {
125
- "hash": "bb154fbf806841bfc2a1885eddc453bf0c291b48fde8e39c68ce5309cd2b2fdd",
125
+ "hash": "1ac04cbce1e85009ecdf224cdbee4fe64bcc80232366c2122805e9c9c3191f9a",
126
126
  "size": 1217,
127
127
  "type": "md"
128
128
  },
@@ -377,23 +377,23 @@
377
377
  "type": "js"
378
378
  },
379
379
  "i18n/en.yaml": {
380
- "hash": "7fb19ab9fed900f10aeaa5bcffe3a4c2037b92513ec593fffc0509cd25f612fb",
381
- "size": 4549,
380
+ "hash": "5eb0e9dfa7baec166fc12ca8a1c0e9623c9a023c51c09455b80a6d8df1e0205f",
381
+ "size": 4941,
382
382
  "type": "yaml"
383
383
  },
384
384
  "i18n/es.yaml": {
385
- "hash": "f83cc1afbe9e7f1ed0243326d4587ac087f72e5b7f9ba667ae8c65a4842a01f4",
386
- "size": 4794,
385
+ "hash": "1ed8c732c13c23e3fc2d5578526c1285a2a1ffc1d79b85d8e12365c3bcbbd882",
386
+ "size": 5225,
387
387
  "type": "yaml"
388
388
  },
389
389
  "i18n/fr.yaml": {
390
- "hash": "d2b3b9077ee93260133ab83e9d928043d38f454ac28444e6d2dbc87fedc5b84c",
391
- "size": 4878,
390
+ "hash": "ae88b914ea39d6613879aae3a3fcdf92a771d3df0f207def6cbb2cc76455cfe9",
391
+ "size": 5290,
392
392
  "type": "yaml"
393
393
  },
394
394
  "i18n/pt.yaml": {
395
- "hash": "10183e9096376f050350cf5f69d582fae2fb47da4eb18fe72486760d5f965e07",
396
- "size": 4747,
395
+ "hash": "9e766ca8c7eba6362816bdc9d1f6162d615f1b5ca1a9b2f75071a53b7ab9f6b3",
396
+ "size": 5163,
397
397
  "type": "yaml"
398
398
  },
399
399
  "intelligence/confidence.yaml": {
@@ -1347,5 +1347,5 @@
1347
1347
  "type": "yaml"
1348
1348
  }
1349
1349
  },
1350
- "version": "4.5.1"
1350
+ "version": "4.5.2"
1351
1351
  }
@@ -1 +1 @@
1
- 9pBBKgpxLVVwfO6C1eWcXgEIDlAm3sGh30KGwtKCy8wc5owlP5ZSbrCIm30FCl6MsK6c6Jktbz/EAZ48qUuCAQ==
1
+ Y5XGGQ07UbiwrVZytFSTJvXNifA8l16S9QI+ret6AVjrYfWvN6E5ChUG0BGKWLsE2wpjVtEW4TVJosYaFDANAg==
@@ -30,6 +30,7 @@ function readClock(clock) {
30
30
  export function validateCapabilitySnapshot(snapshot, { clock } = {}) {
31
31
  assertObject(snapshot, 'INVALID_CATALOG', 'snapshot');
32
32
  assertString(snapshot.snapshot_id, 'INVALID_CATALOG', 'snapshot_id');
33
+ if (snapshot.published_at !== undefined) parseTime(snapshot.published_at, 'INVALID_CATALOG', 'published_at');
33
34
  const expiresAt = parseTime(snapshot.expires_at, 'INVALID_CATALOG', 'expires_at');
34
35
  if (expiresAt <= readClock(clock)) throw new ContractError('CATALOG_SNAPSHOT_EXPIRED', 'capability snapshot is expired', { snapshot_id: snapshot.snapshot_id });
35
36
  if (!Array.isArray(snapshot.models) || snapshot.models.length === 0) throw new ContractError('INVALID_CATALOG', 'models must be a non-empty array');
@@ -49,6 +50,15 @@ export function validateCapabilitySnapshot(snapshot, { clock } = {}) {
49
50
  } else if (model.highest_reasoning_configuration !== undefined) {
50
51
  assertString(model.highest_reasoning_configuration, 'INVALID_CATALOG', 'model.highest_reasoning_configuration');
51
52
  }
53
+ if (model.routing_priority !== undefined) {
54
+ assertObject(model.routing_priority, 'INVALID_CATALOG', 'model.routing_priority');
55
+ for (const [route, priority] of Object.entries(model.routing_priority)) {
56
+ assertString(route, 'INVALID_CATALOG', 'model.routing_priority key');
57
+ if (!Number.isFinite(priority) || priority < 0) throw new ContractError('INVALID_CATALOG', 'model.routing_priority values must be non-negative numbers');
58
+ }
59
+ }
60
+ if (model.tier !== undefined) assertString(model.tier, 'INVALID_CATALOG', 'model.tier');
61
+ if (model.natural_role !== undefined) assertString(model.natural_role, 'INVALID_CATALOG', 'model.natural_role');
52
62
  }
53
63
  return Object.freeze(canonicalize(snapshot));
54
64
  }
@@ -91,11 +101,17 @@ export function validateInstallation(installation, snapshot, { bindingMap = DEFA
91
101
  selectedHarnesses.add(binding.harness_id);
92
102
  selectedBindings.set(`${binding.provider_id}:${binding.harness_id}`, binding);
93
103
  }
94
- assertObject(installation.primary_binding, 'INVALID_INSTALLATION', 'primary_binding');
95
- const primaryKey = `${installation.primary_binding.provider_id}:${installation.primary_binding.harness_id}`;
96
- if (!selectedBindings.has(primaryKey)) throw new ContractError('PRIMARY_BINDING_NOT_SELECTED', 'primary_binding must be selected');
97
- if (installation.primary_harness !== undefined && installation.primary_harness !== installation.primary_binding.harness_id) {
98
- throw new ContractError('PRIMARY_HARNESS_MISMATCH', 'primary_harness must match primary_binding.harness_id');
104
+ // v2.1 installations are symmetric across enabled CLIs. Legacy v2 artifacts
105
+ // may retain a primary binding, which remains validated but is never required.
106
+ if (installation.primary_binding !== undefined) {
107
+ assertObject(installation.primary_binding, 'INVALID_INSTALLATION', 'primary_binding');
108
+ const primaryKey = `${installation.primary_binding.provider_id}:${installation.primary_binding.harness_id}`;
109
+ if (!selectedBindings.has(primaryKey)) throw new ContractError('PRIMARY_BINDING_NOT_SELECTED', 'primary_binding must be selected');
110
+ if (installation.primary_harness !== undefined && installation.primary_harness !== installation.primary_binding.harness_id) {
111
+ throw new ContractError('PRIMARY_HARNESS_MISMATCH', 'primary_harness must match primary_binding.harness_id');
112
+ }
113
+ } else if (installation.primary_harness !== undefined) {
114
+ throw new ContractError('PRIMARY_BINDING_MISSING', 'legacy primary_harness requires primary_binding');
99
115
  }
100
116
  if (installation.profile === 'focus-ai-internal' && installation.external_integrations?.clickup !== 'required') {
101
117
  throw new ContractError('PROFILE_DEPENDENCY_MISSING', 'focus-ai-internal requires ClickUp');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.5.1",
3
+ "version": "4.5.2",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System - Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@ const FRAMEWORK_SOURCE = existsSync(BUNDLED_SOURCE) ? BUNDLED_SOURCE : MONOREPO_
23
23
  * Install Chati.dev framework into target directory
24
24
  */
25
25
  export async function installFramework(config) {
26
- const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, llmProvider, allProviders } = config;
26
+ const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, allProviders } = config;
27
27
  // Always derive version from package.json (single source of truth)
28
28
  let { version } = config;
29
29
  if (!version) {
@@ -55,7 +55,7 @@ export async function installFramework(config) {
55
55
  if (!existsSync(sessionPath)) {
56
56
  writeFileSync(
57
57
  sessionPath,
58
- generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders }),
58
+ generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, allProviders }),
59
59
  'utf-8'
60
60
  );
61
61
  }
@@ -114,12 +114,13 @@ export async function installFramework(config) {
114
114
  createDir(join(memoriesBase, dir));
115
115
  }
116
116
 
117
- // Copy framework files from source (adapted for non-Claude providers)
118
- copyFrameworkFiles(frameworkDir, llmProvider || 'claude');
117
+ // Copy the canonical framework as a provider-neutral fallback. Every enabled
118
+ // CLI receives its own explicit adapted overlay below.
119
+ copyFrameworkFiles(frameworkDir, 'claude');
119
120
 
120
- // Generate provider overlays for secondary CLI providers
121
- if (allProviders && allProviders.length > 1) {
122
- generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, llmProvider || 'claude', allProviders);
121
+ // Generate symmetric provider overlays. No CLI is permanently primary.
122
+ if (allProviders && allProviders.length > 0) {
123
+ generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, allProviders);
123
124
  }
124
125
 
125
126
  // Bundle the CLI source into the framework dir so chati-router.js can run
@@ -159,19 +160,19 @@ export async function installFramework(config) {
159
160
  // Write config.yaml
160
161
  writeFileSync(
161
162
  join(frameworkDir, 'config.yaml'),
162
- generateConfigYaml({ version, projectType, language, selectedIDEs, llmProvider, allProviders }),
163
+ generateConfigYaml({ version, projectType, language, selectedIDEs, allProviders }),
163
164
  'utf-8'
164
165
  );
165
166
 
166
- // 3. Configure IDEs (secondary providers get overlay orchestrator path)
167
- const primary = llmProvider || 'claude';
167
+ // 3. Configure every CLI against its own overlay. The CLI that opens a
168
+ // session becomes that session's orchestrator.
168
169
  for (const ideKey of selectedIDEs) {
169
170
  const ideProvider = IDE_TO_PROVIDER[ideKey];
170
- const isSecondary = ideProvider && ideProvider !== primary && allProviders && allProviders.length > 1;
171
- const orchestratorPath = isSecondary
171
+ const hasProviderOverlay = ideProvider && allProviders?.includes(ideProvider);
172
+ const orchestratorPath = hasProviderOverlay
172
173
  ? `.chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
173
174
  : '.chati.dev/orchestrator/chati.md';
174
- await configureIDE(targetDir, ideKey, selectedMCPs, { orchestratorPath, isSecondary, providerName: ideProvider });
175
+ await configureIDE(targetDir, ideKey, selectedMCPs, { orchestratorPath, isSecondary: false, providerName: ideProvider });
175
176
  }
176
177
 
177
178
  const hasClaude = selectedIDEs.includes('claude-code');
@@ -2,8 +2,8 @@
2
2
  * @fileoverview Provider Overlay Generator for multi-CLI continuity.
3
3
  *
4
4
  * When multiple CLI providers are selected (e.g. Claude Code + Gemini CLI),
5
- * the primary provider's adapted files go in chati.dev/ (main). For each
6
- * secondary provider, this module generates an overlay directory at
5
+ * the provider-neutral framework lives in chati.dev/ (main). For each
6
+ * enabled provider, this module generates an overlay directory at
7
7
  * chati.dev/.adapted/<provider>/ with the ADAPTABLE_FILES adapted for
8
8
  * that provider. Each CLI reads only its own files — zero runtime translation.
9
9
  *
@@ -15,31 +15,24 @@ import { join, dirname } from 'path';
15
15
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
16
16
 
17
17
  /**
18
- * Generate adapted framework overlays for secondary providers.
18
+ * Generate adapted framework overlays for all enabled providers.
19
19
  *
20
- * For each non-primary CLI provider, creates chati.dev/.adapted/<provider>/
20
+ * For each CLI provider, creates chati.dev/.adapted/<provider>/
21
21
  * with provider-adapted copies of all ADAPTABLE_FILES.
22
22
  *
23
- * Reads CANONICAL (Claude) content from frameworkSource, then adapts for
24
- * each secondary provider. This avoids double-adaptation issues when the
25
- * primary provider is not Claude.
23
+ * Reads canonical source content from frameworkSource, then adapts it for
24
+ * each enabled provider. Every CLI resolves its own overlay symmetrically.
26
25
  *
27
26
  * @param {string} targetDir - Project root directory
28
27
  * @param {string} frameworkSource - Source directory with canonical framework files
29
- * @param {string} primaryProvider - Primary provider name ('claude', 'gemini', 'codex')
30
28
  * @param {string[]} allProviders - All CLI provider names
31
29
  * @returns {{ generated: string[], skipped: string[] }}
32
30
  */
33
- export function generateProviderOverlays(targetDir, frameworkSource, primaryProvider, allProviders) {
31
+ export function generateProviderOverlays(targetDir, frameworkSource, allProviders) {
34
32
  const result = { generated: [], skipped: [] };
35
33
  const frameworkDir = join(targetDir, '.chati.dev');
36
34
 
37
35
  for (const provider of allProviders) {
38
- if (provider === primaryProvider) {
39
- result.skipped.push(provider);
40
- continue;
41
- }
42
-
43
36
  const overlayDir = join(frameworkDir, '.adapted', provider);
44
37
 
45
38
  for (const file of ADAPTABLE_FILES) {
@@ -64,7 +57,7 @@ export function generateProviderOverlays(targetDir, frameworkSource, primaryProv
64
57
  /**
65
58
  * Resolve the correct framework file path for a given provider.
66
59
  *
67
- * Returns the overlay path for secondary providers (if it exists),
60
+ * Returns the provider overlay path when it exists,
68
61
  * otherwise falls back to the main chati.dev/ path.
69
62
  *
70
63
  * @param {string} projectDir - Project root directory
@@ -4,7 +4,7 @@ import * as yaml from 'js-yaml';
4
4
  * Generate session.yaml content
5
5
  */
6
6
  export function generateSessionYaml(config) {
7
- const { projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders } = config;
7
+ const { projectName, projectType, language, selectedIDEs, selectedMCPs, allProviders } = config;
8
8
 
9
9
  // Schema MUST match session-manager.js DEFAULT_SESSION fields,
10
10
  // otherwise validateSession() rejects installer-generated sessions.
@@ -27,9 +27,7 @@ export function generateSessionYaml(config) {
27
27
  user_level_confidence: 0.0,
28
28
  ides: selectedIDEs,
29
29
  mcps: selectedMCPs,
30
- providers_enabled: allProviders && allProviders.length > 0 ? allProviders : [llmProvider || 'claude'],
31
- primary_provider: llmProvider || 'claude',
32
- active_provider: llmProvider || 'claude',
30
+ providers_enabled: allProviders && allProviders.length > 0 ? allProviders : ['claude'],
33
31
  completed_agents: [],
34
32
  agent_results: {},
35
33
  mode_transitions: [],
@@ -102,10 +100,9 @@ export const PROVIDER_MODEL_MAPS = {
102
100
  * Generate config.yaml content
103
101
  */
104
102
  export function generateConfigYaml(config) {
105
- const { version, projectType, language, llmProvider, allProviders } = config;
103
+ const { version, projectType, language, allProviders } = config;
106
104
  const selectedIDEs = config.selectedIDEs || [];
107
- const provider = llmProvider || 'claude';
108
- const providers = allProviders && allProviders.length > 0 ? allProviders : [provider];
105
+ const providers = allProviders && allProviders.length > 0 ? allProviders : ['claude'];
109
106
 
110
107
  const configData = {
111
108
  version: version,
@@ -116,9 +113,10 @@ export function generateConfigYaml(config) {
116
113
  language: language,
117
114
  ides: selectedIDEs,
118
115
  providers: {
119
- claude: { enabled: providers.includes('claude') || provider === 'claude', primary: provider === 'claude' },
120
- gemini: { enabled: providers.includes('gemini') || selectedIDEs.includes('gemini-cli'), model_default: 'pro', primary: provider === 'gemini' },
121
- codex: { enabled: providers.includes('codex') || selectedIDEs.includes('codex-cli'), model_default: 'codex', primary: provider === 'codex' },
116
+ claude: { enabled: providers.includes('claude') },
117
+ gemini: { enabled: providers.includes('gemini') || selectedIDEs.includes('gemini-cli') },
118
+ codex: { enabled: providers.includes('codex') || selectedIDEs.includes('codex-cli') },
119
+ grok: { enabled: providers.includes('grok') || selectedIDEs.includes('grok-cli') },
122
120
  },
123
121
  };
124
122
 
@@ -148,7 +146,7 @@ export function generateConfigYaml(config) {
148
146
  frustration_detection: true,
149
147
  bash_security_checks: true,
150
148
  // Agent Teams (Article XXI) — default ON in v4.2.2.
151
- // Only effective when provider === 'claude' (gated in cli.js
149
+ // Only effective when the active session runs in Claude (gated in cli.js
152
150
  // isAgentTeamsEnabled). Gemini and Codex always fall back to sequential
153
151
  // pipeline silently. See plan: was previously false for "safe rollout"
154
152
  // but the v4.2.0 launch never actually shipped, so this is a fresh start.
@@ -165,17 +163,6 @@ export function generateConfigYaml(config) {
165
163
  anonymous_id: null,
166
164
  };
167
165
 
168
- // Add agent_overrides with optimal model map for the selected provider
169
- if (provider !== 'claude') {
170
- const modelMap = PROVIDER_MODEL_MAPS[provider];
171
- if (modelMap) {
172
- configData.agent_overrides = {};
173
- for (const [agent, model] of Object.entries(modelMap.agents)) {
174
- configData.agent_overrides[agent] = { provider, model };
175
- }
176
- }
177
- }
178
-
179
166
  return yaml.dump(configData, { lineWidth: -1, quoteStyle: 'double', forceQuotes: false });
180
167
  }
181
168
 
@@ -187,28 +187,23 @@ export async function validateInstallation(targetDir) {
187
187
  try {
188
188
  const configContent = readFileSync(configYamlPath, 'utf-8');
189
189
  const enabledProviders = [];
190
- let primaryProvider = null;
191
190
  let currentProvider = null;
192
191
 
193
192
  for (const line of configContent.split('\n')) {
194
193
  const provLine = line.match(/^\s{4}(\w+):$/);
195
- if (provLine && ['claude', 'gemini', 'codex'].includes(provLine[1])) {
194
+ if (provLine && ['claude', 'gemini', 'codex', 'grok'].includes(provLine[1])) {
196
195
  currentProvider = provLine[1];
197
196
  }
198
197
  if (currentProvider && line.includes('enabled: true')) {
199
198
  enabledProviders.push(currentProvider);
200
199
  }
201
- if (currentProvider && line.includes('primary: true')) {
202
- primaryProvider = currentProvider;
203
- }
204
200
  }
205
201
 
206
- // Validate overlay completeness for each secondary provider
207
- if (enabledProviders.length > 1 && primaryProvider) {
208
- const secondaryProviders = enabledProviders.filter(p => p !== primaryProvider);
202
+ // Validate overlay completeness for every enabled provider.
203
+ if (enabledProviders.length > 0) {
209
204
  const overlayDetails = [];
210
205
 
211
- for (const provider of secondaryProviders) {
206
+ for (const provider of enabledProviders) {
212
207
  let present = 0;
213
208
  let missing = 0;
214
209
  const missingFiles = [];
@@ -84,10 +84,17 @@ function readBundledPayload() {
84
84
  };
85
85
  }
86
86
 
87
+ function catalogPublishedAt(catalog) {
88
+ const parsed = catalog.published_at ? Date.parse(catalog.published_at) : 0;
89
+ return Number.isNaN(parsed) ? 0 : parsed;
90
+ }
91
+
92
+ const SOURCE_PRIORITY = Object.freeze({ remote: 3, cache: 2, bundled: 1 });
93
+
87
94
  /**
88
- * Resolves a signed CHATI model catalog. Order is remote, verified local cache,
89
- * then the signed catalog bundled with the installed release. Vendor APIs and
90
- * local binary inference are deliberately outside this trust boundary.
95
+ * Resolves every available signed CHATI model catalog and selects the newest
96
+ * published snapshot. Remote wins ties, then cache, then the bundled release.
97
+ * Vendor APIs and local binary inference stay outside this trust boundary.
91
98
  */
92
99
  export async function resolveCapabilityCatalog({
93
100
  projectDir,
@@ -98,6 +105,7 @@ export async function resolveCapabilityCatalog({
98
105
  timeoutMs = 5000,
99
106
  } = {}) {
100
107
  const failures = [];
108
+ const candidates = [];
101
109
 
102
110
  if (catalogUrl && typeof fetchImpl === 'function') {
103
111
  try {
@@ -109,8 +117,7 @@ export async function resolveCapabilityCatalog({
109
117
  if (!response.ok) throw catalogError('CATALOG_HTTP_ERROR', `catalog endpoint returned HTTP ${response.status}`);
110
118
  const payload = parseSignedPayload(await response.json());
111
119
  const catalog = verifySignedCapabilityCatalog(payload, { clock, publicKeyPem });
112
- const cache_path = projectDir ? writeCache(projectDir, payload) : null;
113
- return Object.freeze({ source: 'remote', source_url: safeCatalogUrl, cache_path, catalog });
120
+ candidates.push({ source: 'remote', source_url: safeCatalogUrl, payload, catalog });
114
121
  } catch (error) {
115
122
  failures.push({ source: 'remote', code: error.code || error.name || 'CATALOG_FETCH_FAILED' });
116
123
  }
@@ -122,7 +129,7 @@ export async function resolveCapabilityCatalog({
122
129
  try {
123
130
  const payload = readSignedCatalogFile(cachePath);
124
131
  const catalog = verifySignedCapabilityCatalog(payload, { clock, publicKeyPem });
125
- return Object.freeze({ source: 'cache', cache_path: CATALOG_CACHE_PATH, catalog });
132
+ candidates.push({ source: 'cache', cache_path: CATALOG_CACHE_PATH, payload, catalog });
126
133
  } catch (error) {
127
134
  failures.push({ source: 'cache', code: error.code || error.name || 'CATALOG_CACHE_INVALID' });
128
135
  }
@@ -132,10 +139,27 @@ export async function resolveCapabilityCatalog({
132
139
  try {
133
140
  const payload = readBundledPayload();
134
141
  const catalog = verifySignedCapabilityCatalog(payload, { clock, publicKeyPem });
135
- return Object.freeze({ source: 'bundled', cache_path: null, catalog });
142
+ candidates.push({ source: 'bundled', cache_path: null, payload, catalog });
136
143
  } catch (error) {
137
144
  failures.push({ source: 'bundled', code: error.code || error.name || 'CATALOG_BUNDLE_INVALID' });
138
145
  }
139
146
 
140
- throw catalogError('CATALOG_UNAVAILABLE', 'no fresh, signed CHATI model catalog is available', { failures });
147
+ if (candidates.length === 0) {
148
+ throw catalogError('CATALOG_UNAVAILABLE', 'no fresh, signed CHATI model catalog is available', { failures });
149
+ }
150
+
151
+ candidates.sort((left, right) =>
152
+ catalogPublishedAt(right.catalog) - catalogPublishedAt(left.catalog)
153
+ || SOURCE_PRIORITY[right.source] - SOURCE_PRIORITY[left.source]
154
+ );
155
+ const selected = candidates[0];
156
+ const cache_path = selected.source === 'remote' && projectDir
157
+ ? writeCache(projectDir, selected.payload)
158
+ : (selected.cache_path ?? null);
159
+ return Object.freeze({
160
+ source: selected.source,
161
+ ...(selected.source_url ? { source_url: selected.source_url } : {}),
162
+ cache_path,
163
+ catalog: selected.catalog,
164
+ });
141
165
  }
@@ -88,12 +88,15 @@ function artifactCore(input, createdAt) {
88
88
  expires_at: input.capability_snapshot.expires_at,
89
89
  models: input.capability_snapshot.models
90
90
  .filter((model) => selectedModelKeys.has(`${model.provider_id}:${model.model_id}`))
91
- .map(({ provider_id, model_id, actions, adjudication_priority, highest_reasoning_configuration }) => ({
91
+ .map(({ provider_id, model_id, actions, adjudication_priority, highest_reasoning_configuration, routing_priority, tier, natural_role }) => ({
92
92
  provider_id,
93
93
  model_id,
94
94
  actions,
95
95
  ...(adjudication_priority !== undefined ? { adjudication_priority } : {}),
96
96
  ...(highest_reasoning_configuration !== undefined ? { highest_reasoning_configuration } : {}),
97
+ ...(routing_priority !== undefined ? { routing_priority } : {}),
98
+ ...(tier !== undefined ? { tier } : {}),
99
+ ...(natural_role !== undefined ? { natural_role } : {}),
97
100
  })),
98
101
  };
99
102
  return {