chati-dev 4.5.13 → 4.5.15

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 (57) hide show
  1. package/framework/agents/build/dev.md +4 -5
  2. package/framework/agents/deploy/devops.md +9 -10
  3. package/framework/agents/discover/brief.md +7 -13
  4. package/framework/agents/discover/brownfield-wu.md +5 -11
  5. package/framework/agents/discover/greenfield-wu.md +7 -12
  6. package/framework/agents/plan/architect.md +1 -1
  7. package/framework/agents/plan/detail.md +5 -5
  8. package/framework/agents/plan/phases.md +4 -5
  9. package/framework/agents/plan/tasks.md +4 -5
  10. package/framework/agents/plan/ux-brand-architect.md +1 -1
  11. package/framework/agents/plan/ux.md +1 -1
  12. package/framework/agents/quality/qa-implementation.md +4 -5
  13. package/framework/agents/quality/qa-planning.md +4 -5
  14. package/framework/agents/quality/qa-visual.md +4 -4
  15. package/framework/agents/shared/visualizer.md +1 -1
  16. package/framework/config.yaml +5 -5
  17. package/framework/constitution.md +24 -26
  18. package/framework/context/governance.md +6 -6
  19. package/framework/context/root.md +4 -3
  20. package/framework/data/entity-registry.yaml +3 -3
  21. package/framework/data/model-limits.json +5 -2
  22. package/framework/hooks/model-governance.js +3 -2
  23. package/framework/hooks/prism-engine.js +1 -1
  24. package/framework/i18n/en.yaml +6 -2
  25. package/framework/i18n/es.yaml +6 -2
  26. package/framework/i18n/fr.yaml +6 -2
  27. package/framework/i18n/pt.yaml +6 -2
  28. package/framework/intelligence/context-engine.md +12 -16
  29. package/framework/manifest.json +63 -63
  30. package/framework/manifest.sig +1 -1
  31. package/framework/orchestrator/chati-router.js +40 -6
  32. package/framework/orchestrator/chati.md +12 -13
  33. package/framework/schemas/session.schema.json +1 -1
  34. package/package.json +1 -1
  35. package/src/config/context-file-generator.js +54 -246
  36. package/src/config/framework-adapter.js +45 -208
  37. package/src/context/bracket-tracker.js +3 -3
  38. package/src/context/engine.js +1 -1
  39. package/src/installer/core.js +190 -66
  40. package/src/installer/provider-overlay.js +1 -1
  41. package/src/installer/templates.js +55 -65
  42. package/src/installer-v2/index.js +2 -0
  43. package/src/memory/magic-docs.js +45 -26
  44. package/src/orchestrator/cli.js +50 -45
  45. package/src/orchestrator/handoff-engine.js +7 -5
  46. package/src/orchestrator/session-manager.js +24 -3
  47. package/src/terminal/cli-registry.js +5 -5
  48. package/src/terminal/prompt-builder.js +14 -10
  49. package/src/terminal/run-agent.js +10 -6
  50. package/src/terminal/run-parallel.js +6 -2
  51. package/src/terminal/run-team.js +7 -2
  52. package/src/terminal/spawner.js +34 -42
  53. package/src/utils/config-parser.js +2 -2
  54. package/src/utils/provider-limits.js +3 -1
  55. package/src/wizard/feedback.js +11 -0
  56. package/src/wizard/i18n.js +6 -2
  57. package/src/wizard/index.js +17 -7
@@ -3,7 +3,7 @@
3
3
  * CLI runner for single-agent terminal execution.
4
4
  *
5
5
  * Called by the orchestrator via the Bash tool to spawn an agent
6
- * in a separate Claude Code process with the correct model.
6
+ * in a separate routed provider process with the exact model binding.
7
7
  *
8
8
  * Usage:
9
9
  * node run-agent.js --agent detail --task-id expand-prd \
@@ -104,7 +104,11 @@ async function main() {
104
104
  }
105
105
 
106
106
  // Wait for rate limit slot before spawning
107
- const spawnProvider = promptResult.provider || args.provider || 'claude';
107
+ const spawnProvider = promptResult.provider || args.provider;
108
+ if (!spawnProvider) {
109
+ outputError('No routed provider was supplied for agent execution');
110
+ process.exit(1);
111
+ }
108
112
  const readiness = checkProviderReadiness(spawnProvider, { workingDir: projectDir });
109
113
  if (!readiness.ready) {
110
114
  outputResult({ status: 'error', error: readiness.detail, code: readiness.code, provider: spawnProvider });
@@ -164,7 +168,7 @@ async function main() {
164
168
  const costRecord = tracker.recordExecution({
165
169
  agent: args.agent,
166
170
  model: promptResult.model,
167
- provider: promptResult.provider || args.provider || 'claude',
171
+ provider: spawnProvider,
168
172
  taskId: args['task-id'],
169
173
  inputText: promptResult.prompt || '',
170
174
  outputText: stdout,
@@ -227,7 +231,7 @@ async function main() {
227
231
  status: parsed.handoff.status,
228
232
  agent: args.agent,
229
233
  model: promptResult.model,
230
- provider: promptResult.provider || args.provider || 'claude',
234
+ provider: spawnProvider,
231
235
  reasoningConfiguration: args['reasoning-configuration'] || null,
232
236
  exitCode: handle.exitCode,
233
237
  handoff: parsed.handoff,
@@ -242,7 +246,7 @@ async function main() {
242
246
  status: 'needs_input',
243
247
  agent: args.agent,
244
248
  model: promptResult.model,
245
- provider: promptResult.provider || args.provider || 'claude',
249
+ provider: spawnProvider,
246
250
  exitCode: handle.exitCode,
247
251
  handoff: recoverInteractiveHandoff(args.agent, stdout, handle.exitCode),
248
252
  contractRecovery: 'interactive_output_wrapped',
@@ -257,7 +261,7 @@ async function main() {
257
261
  code: parsed.found ? 'INVALID_HANDOFF' : 'MISSING_HANDOFF',
258
262
  agent: args.agent,
259
263
  model: promptResult.model,
260
- provider: promptResult.provider || args.provider || 'claude',
264
+ provider: spawnProvider,
261
265
  exitCode: handle.exitCode,
262
266
  handoff: parsed.handoff,
263
267
  handoffWarnings: parsed.warnings,
@@ -138,7 +138,11 @@ async function main() {
138
138
  }
139
139
 
140
140
  // Check rate limit capacity before spawning
141
- const groupProvider = configs[0]?.provider || 'claude';
141
+ const groupProvider = configs[0]?.provider;
142
+ if (!groupProvider) {
143
+ outputError('No routed provider was supplied for parallel execution');
144
+ process.exit(1);
145
+ }
142
146
  const limiter = getRateLimiter(groupProvider);
143
147
  const rateStats = limiter.getStats();
144
148
  const availableSlots = rateStats.limit - rateStats.used;
@@ -197,7 +201,7 @@ async function main() {
197
201
  return {
198
202
  agent: cfg.agent,
199
203
  model: modelKey,
200
- provider: cfg.provider || 'claude',
204
+ provider: cfg.provider || 'unbound',
201
205
  inputTokens,
202
206
  outputTokens,
203
207
  estimatedCost: ((inputTokens + outputTokens) / 1000) * rate,
@@ -86,7 +86,7 @@ async function main() {
86
86
  const teamType = args['team-type'];
87
87
  const projectDir = args['project-dir'] || process.cwd();
88
88
  const previousAgent = args['previous-agent'] || null;
89
- const provider = args.provider || 'claude';
89
+ const provider = args.provider || null;
90
90
  const timeout = parseInt(args.timeout, 10) || 1_800_000; // default 30 minutes
91
91
 
92
92
  if (!teamId || !teamType) {
@@ -94,6 +94,11 @@ async function main() {
94
94
  process.exit(1);
95
95
  }
96
96
 
97
+ if (!provider) {
98
+ outputError('Missing required argument: --provider');
99
+ process.exit(1);
100
+ }
101
+
97
102
  const members = TEAM_MEMBERS[teamType];
98
103
  const taskIds = TEAM_TASK_IDS[teamType];
99
104
 
@@ -248,7 +253,7 @@ async function main() {
248
253
  return {
249
254
  agent: cfg.agent,
250
255
  model: modelKey,
251
- provider: cfg.provider || 'claude',
256
+ provider: cfg.provider,
252
257
  inputTokens,
253
258
  outputTokens,
254
259
  estimatedCost: ((inputTokens + outputTokens) / 1000) * rate,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Terminal spawner for multi-agent parallel execution.
3
3
  *
4
- * Spawns separate Claude Code CLI processes so that multiple agents
4
+ * Spawns separate provider CLI processes so that multiple agents
5
5
  * can work concurrently. The heavy lifting is split into pure,
6
6
  * testable helpers (buildSpawnCommand) and a thin runtime layer
7
7
  * (spawnTerminal) that actually calls child_process.spawn.
@@ -112,7 +112,6 @@ export function cleanParentEnv(env) {
112
112
  * @property {string} [providerId] - Vendor provider identifier (anthropic, openai, xai)
113
113
  * @property {string} [reasoningConfiguration] - Requested reasoning effort
114
114
  * @property {string} [catalogSnapshotRef] - Capability catalog snapshot used for routing
115
- * @property {boolean} [strictProvider] - Reject a missing provider rather than using the legacy fallback
116
115
  * @property {string} [prompt] - Full prompt string (from prompt-builder, piped via stdin)
117
116
  * @property {object} [contextPayload] - Context to inject via env var
118
117
  * @property {string[]} [writeScope] - Override write scope
@@ -137,7 +136,7 @@ export function cleanParentEnv(env) {
137
136
 
138
137
  /**
139
138
  * Build the CLI command, arguments and environment for spawning a
140
- * Claude Code terminal. This is a **pure function** -- it does not
139
+ * provider CLI terminal. This is a **pure function** -- it does not
141
140
  * perform any I/O and is therefore fully testable in isolation.
142
141
  *
143
142
  * @param {SpawnConfig} config
@@ -153,6 +152,11 @@ export function buildSpawnCommand(config) {
153
152
  if (!config.taskId || typeof config.taskId !== 'string') {
154
153
  throw new Error('config.taskId is required and must be a string');
155
154
  }
155
+ if (!config.provider || typeof config.provider !== 'string') {
156
+ const failure = new Error('config.provider is required and must be a string');
157
+ failure.code = 'PROVIDER_REQUIRED';
158
+ throw failure;
159
+ }
156
160
 
157
161
  const terminalId = generateTerminalId(config.agent);
158
162
  const isolationEnv = buildIsolationEnv(config.agent);
@@ -175,13 +179,14 @@ export function buildSpawnCommand(config) {
175
179
  }
176
180
  }
177
181
 
178
- // Resolve CLI provider defaults to claude for backwards compatibility
179
- const providerName = config.provider || 'claude';
182
+ // Resolve the exact provider selected by the router. Never substitute a
183
+ // different vendor when credentials, catalog data, or a CLI are missing.
184
+ const providerName = config.provider;
180
185
  let command, args, prompt;
181
- let providerFallback = null;
182
- let actualProvider = providerName;
186
+ const providerFallback = null;
187
+ const actualProvider = providerName;
183
188
  let actualModel;
184
- let actualReasoningConfiguration = null;
189
+ let actualReasoningConfiguration;
185
190
 
186
191
  try {
187
192
  const provider = getProvider(providerName);
@@ -194,32 +199,9 @@ export function buildSpawnCommand(config) {
194
199
  actualModel = adapterResult.effectiveModel;
195
200
  actualReasoningConfiguration = adapterResult.effectiveReasoningConfiguration;
196
201
  } catch (err) {
197
- if (config.strictProvider) {
198
- const failure = new Error(`Selected provider "${providerName}" is unavailable: ${err.message}`);
199
- failure.code = 'SELECTED_PROVIDER_UNAVAILABLE';
200
- throw failure;
201
- }
202
- // Fallback to claude if provider resolution fails (backwards compatibility)
203
- providerFallback = {
204
- requested: providerName,
205
- actual: 'claude',
206
- reason: err.message,
207
- timestamp: new Date().toISOString(),
208
- };
209
- actualProvider = 'claude';
210
- console.error(`[chati] Provider "${providerName}" resolution failed: ${err.message}. Falling back to claude.`);
211
- command = 'claude';
212
- args = ['--print', '--dangerously-skip-permissions'];
213
- const claudeProvider = getProvider('claude');
214
- const requestedClaudeModel = config.model || 'sonnet';
215
- actualModel = claudeProvider.modelMap[requestedClaudeModel]
216
- || (requestedClaudeModel.startsWith('claude-') ? requestedClaudeModel : claudeProvider.modelMap.sonnet);
217
- args.push('--model', actualModel);
218
- if (config.reasoningConfiguration) {
219
- args.push('--effort', config.reasoningConfiguration);
220
- actualReasoningConfiguration = config.reasoningConfiguration;
221
- }
222
- prompt = config.prompt || null;
202
+ const failure = new Error(`Selected provider "${providerName}" is unavailable: ${err.message}`);
203
+ failure.code = 'SELECTED_PROVIDER_UNAVAILABLE';
204
+ throw failure;
223
205
  }
224
206
 
225
207
  return {
@@ -247,10 +229,10 @@ export function spawnTerminal(config) {
247
229
  agent: config.agent,
248
230
  taskId: config.taskId,
249
231
  provider: actualProvider,
250
- providerId: providerFallback ? null : (config.providerId || null),
232
+ providerId: config.providerId || null,
251
233
  model: actualModel,
252
234
  reasoningConfiguration: actualReasoningConfiguration,
253
- catalogSnapshotRef: providerFallback ? null : (config.catalogSnapshotRef || null),
235
+ catalogSnapshotRef: config.catalogSnapshotRef || null,
254
236
  });
255
237
  if (!selectionRecord.saved) {
256
238
  const auditError = new Error(selectionRecord.error || 'Failed to persist model selection audit');
@@ -355,12 +337,22 @@ export function spawnParallelGroup(configs) {
355
337
  throw new Error(`Write scope conflicts detected: ${details}`);
356
338
  }
357
339
 
358
- // Preemptive rate limit capacity check
359
- const groupProvider = configs[0]?.provider || 'claude';
360
- const limiter = getRateLimiter(groupProvider);
361
- const stats = limiter.getStats();
362
- if (stats.used + configs.length > stats.limit) {
363
- console.error(`[chati] Rate limit warning: ${stats.used}/${stats.limit} slots used, requesting ${configs.length} more`);
340
+ // Preemptive capacity check per provider. A parallel group may contain
341
+ // routed tasks from multiple vendors.
342
+ const providerCounts = new Map();
343
+ for (const config of configs) {
344
+ if (!config?.provider || typeof config.provider !== 'string') {
345
+ const failure = new Error('Every parallel config requires an explicit provider');
346
+ failure.code = 'PROVIDER_REQUIRED';
347
+ throw failure;
348
+ }
349
+ providerCounts.set(config.provider, (providerCounts.get(config.provider) || 0) + 1);
350
+ }
351
+ for (const [provider, requested] of providerCounts) {
352
+ const stats = getRateLimiter(provider).getStats();
353
+ if (stats.used + requested > stats.limit) {
354
+ console.error(`[chati] Rate limit warning for ${provider}: ${stats.used}/${stats.limit} slots used, requesting ${requested} more`);
355
+ }
364
356
  }
365
357
 
366
358
  const groupId = `group-${Date.now()}`;
@@ -14,12 +14,12 @@ import { resolveFrameworkDir } from './framework-dir.js';
14
14
  * Parse provider configuration from config.yaml.
15
15
  *
16
16
  * @param {string} projectDir - Project root directory
17
- * @returns {{ primary: string, enabled: string[], raw: string|null }}
17
+ * @returns {{ primary: string|null, enabled: string[], raw: string|null }}
18
18
  */
19
19
  export function parseProviderConfig(projectDir) {
20
20
  const configPath = join(projectDir, resolveFrameworkDir(projectDir), 'config.yaml');
21
21
  if (!existsSync(configPath)) {
22
- return { primary: 'claude', enabled: ['claude'], raw: null };
22
+ return { primary: null, enabled: [], raw: null };
23
23
  }
24
24
 
25
25
  const raw = readFileSync(configPath, 'utf-8');
@@ -38,7 +38,7 @@ function loadLimits() {
38
38
  // branch exists so unit tests that run the module without sync still work.
39
39
  // 2026-04-18 (context-window-auto-detect-v2): opus family is 1M by default.
40
40
  return {
41
- providers: { claude: 1_000_000, gemini: 1_000_000, codex: 128_000 },
41
+ providers: { claude: 1_000_000, gemini: 1_000_000, codex: 128_000, grok: 200_000 },
42
42
  models: {
43
43
  opus: 1_000_000,
44
44
  'opus[1m]': 1_000_000,
@@ -48,6 +48,8 @@ function loadLimits() {
48
48
  pro: 1_000_000,
49
49
  flash: 1_000_000,
50
50
  codex: 128_000,
51
+ 'grok-4.5': 200_000,
52
+ 'grok-4.6': 200_000,
51
53
  },
52
54
  };
53
55
  }
@@ -13,6 +13,17 @@ export function createSpinner(text) {
13
13
  });
14
14
  }
15
15
 
16
+ /** Run one automatic operation with a spinner that always stops. */
17
+ export async function withSpinner(text, operation, { spinnerFactory = createSpinner } = {}) {
18
+ const spinner = spinnerFactory(text);
19
+ spinner.start();
20
+ try {
21
+ return await operation();
22
+ } finally {
23
+ spinner.stop();
24
+ }
25
+ }
26
+
16
27
  /**
17
28
  * Show installation progress step
18
29
  */
@@ -75,7 +75,7 @@ const FALLBACK_EN = {
75
75
  created_commands: 'Created .claude/commands/ (thin router)',
76
76
  installed_constitution: 'Installed Constitution (Articles I-XXV)',
77
77
  created_session: 'Created session.yaml schema',
78
- created_claude_md: 'Created CLAUDE.md',
78
+ created_claude_md: 'Created provider-neutral context and native harness files',
79
79
  configured_mcps: 'Configured MCPs:',
80
80
  validating: 'Validating installation...',
81
81
  agents_valid: 'All agents implement 8 protocols',
@@ -106,6 +106,9 @@ const FALLBACK_EN = {
106
106
  system_prerequisites_failed: 'System prerequisites not met:',
107
107
  fix_prerequisites: 'Fix the issues above and try again.',
108
108
  system_check: 'System check:',
109
+ system_checking: 'Checking system requirements...',
110
+ catalog_resolving: 'Loading the signed model catalog...',
111
+ internal_authorizing: 'Verifying access to Internal mode...',
109
112
  created_provider_claude: 'Created .claude/commands/ (thin router)',
110
113
  created_provider_gemini: 'Created .gemini/commands/ (TOML command)',
111
114
  created_provider_codex: 'Created .agents/skills/chati/ (Codex skill)',
@@ -120,6 +123,7 @@ const FALLBACK_EN = {
120
123
  license_placeholder: 'CHATI-XXXX-XXXX-XXXX',
121
124
  license_skipped: 'Skipped. Run: npx chati-dev activate --key=YOUR-KEY when ready.',
122
125
  license_activating: 'Activating license...',
126
+ license_verifying: 'Verifying saved license...',
123
127
  license_activated: 'License activated! Plan: {plan}, {days} day(s) remaining.',
124
128
  license_activation_failed: 'Activation failed: {error}',
125
129
  license_retry: 'Run: npx chati-dev activate --key=YOUR-KEY to try again.',
@@ -153,7 +157,7 @@ const FALLBACK_EN = {
153
157
  },
154
158
  errors: {
155
159
  session_corrupted: 'Session file appears corrupted. Attempting recovery...',
156
- handoff_missing: 'Handoff not found. Using session.yaml + CLAUDE.md as fallback.',
160
+ handoff_missing: 'Handoff not found. Using session.yaml + .chati/project-context.md as fallback.',
157
161
  agent_failed: 'Agent failed after 3 attempts. Escalating to user.',
158
162
  mcp_required: "Required MCP '{mcp}' is not configured. Installation instructions:",
159
163
  mcp_optional: "Optional MCP '{mcp}' not configured. Skipping related functionality.",
@@ -4,7 +4,7 @@ import { join, dirname, basename } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { logBanner } from '../utils/logger.js';
6
6
  import { WIZARD_BACK, stepLanguage, stepProjectType, stepProviderSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
7
- import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
7
+ import { createSpinner, withSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
9
  import { INSTALLATION_ARTIFACT_PATH, dryRunV2, installV2, reconfigureV2 } from '../installer-v2/index.js';
10
10
  import { buildWizardV2InstallationInput } from '../installer-v2/wizard-installation.js';
@@ -48,7 +48,7 @@ export async function runWizard(targetDir, options = {}) {
48
48
 
49
49
  // Preflight: validate system prerequisites (Node >= 20, npm, Git, Playwright)
50
50
  const { runPreflightCheck } = await import('../installer/preflight.js');
51
- const preflight = await runPreflightCheck();
51
+ const preflight = await withSpinner(t('installer.system_checking'), () => runPreflightCheck());
52
52
  const preflightFmt = preflight.checks.map(c => {
53
53
  const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '⚠' : '✗';
54
54
  return ` ${icon} ${c.message}`;
@@ -128,9 +128,13 @@ export async function runWizard(targetDir, options = {}) {
128
128
  continue;
129
129
  }
130
130
  selectedProviders = value;
131
- catalogResolution ??= options.signedCapabilityCatalogEnvelope
132
- ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
133
- : await resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch });
131
+ if (!catalogResolution) {
132
+ catalogResolution = await withSpinner(t('installer.catalog_resolving'), async () => (
133
+ options.signedCapabilityCatalogEnvelope
134
+ ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
135
+ : resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch })
136
+ ));
137
+ }
134
138
  // Provider selection defines the permitted routing pool. Model discovery
135
139
  // and task-specific choice stay inside the control plane.
136
140
  modelSelections = options.modelSelections;
@@ -149,7 +153,10 @@ export async function runWizard(targetDir, options = {}) {
149
153
  installationMode = value;
150
154
  if (installationMode === 'internal') {
151
155
  try {
152
- await requireLicenseEntitlement('internal', { validate: options.licenseValidator });
156
+ await withSpinner(
157
+ t('installer.internal_authorizing'),
158
+ () => requireLicenseEntitlement('internal', { validate: options.licenseValidator }),
159
+ );
153
160
  } catch (error) {
154
161
  if (options.installationMode !== undefined) throw error;
155
162
  p.log.error(error.message);
@@ -328,7 +335,10 @@ async function runLicenseActivationStep() {
328
335
  const existingKey = getLicenseKey();
329
336
  if (existingKey) {
330
337
  try {
331
- const status = await ensureCurrentMachineLicense({ key: existingKey });
338
+ const status = await withSpinner(
339
+ t('installer.license_verifying'),
340
+ () => ensureCurrentMachineLicense({ key: existingKey }),
341
+ );
332
342
  if (status.status === 'VALID') return;
333
343
  p.log.warn(t('installer.license_saved_invalid', { reason: status.reason || status.status }));
334
344
  } catch (error) {