chati-dev 4.2.0 → 4.2.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.
@@ -22,6 +22,10 @@
22
22
  * Maps each chati.dev hook to its Gemini CLI equivalent event.
23
23
  */
24
24
  export const HOOK_MAP = {
25
+ // license-guard MUST be first on BeforeModel so license check runs before
26
+ // any model call. Without this, an expired trial could keep using Gemini
27
+ // indefinitely from a long-running session.
28
+ 'license-guard': { event: 'BeforeModel', description: 'Validate license on every turn (chati.dev governance)' },
25
29
  'prism-engine': { event: 'BeforeModel', description: 'Inject PRISM context into model prompt' },
26
30
  'model-governance': { event: 'BeforeModel', description: 'Advisory: recommended model per agent' },
27
31
  'mode-governance': { event: 'BeforeTool', description: 'Block writes outside current mode scope' },
@@ -611,6 +615,7 @@ main();
611
615
  */
612
616
  export function generateAllGeminiHooks() {
613
617
  return {
618
+ 'license-guard.js': generateLicenseGuard(),
614
619
  'prism-engine.js': generatePrismEngine(),
615
620
  'model-governance.js': generateModelGovernance(),
616
621
  'mode-governance.js': generateModeGovernance(),
@@ -622,6 +627,59 @@ export function generateAllGeminiHooks() {
622
627
  };
623
628
  }
624
629
 
630
+ /**
631
+ * Generate the license guard hook for Gemini CLI.
632
+ * BeforeModel event — validates license on every turn.
633
+ *
634
+ * Delegates to chati.dev/hooks/license-guard.js (canonical implementation)
635
+ * via dynamic import of checkLicense(). Translates the verdict object to
636
+ * Gemini's blocking schema (exit code 2 + stderr is the universal block).
637
+ */
638
+ function generateLicenseGuard() {
639
+ return `${HOOK_HEADER}
640
+ async function main() {
641
+ let input = '';
642
+ for await (const chunk of process.stdin) {
643
+ input += chunk;
644
+ }
645
+
646
+ try {
647
+ const event = JSON.parse(input);
648
+ const cwd = event.cwd || process.cwd();
649
+
650
+ // Delegate to canonical license check
651
+ const hookPath = join(cwd, 'chati.dev', 'hooks', 'license-guard.js');
652
+ if (!existsSync(hookPath)) {
653
+ // No canonical hook present — fail open
654
+ console.log(JSON.stringify({}));
655
+ return;
656
+ }
657
+
658
+ const mod = await import(hookPath);
659
+ if (typeof mod.checkLicense !== 'function') {
660
+ console.log(JSON.stringify({}));
661
+ return;
662
+ }
663
+
664
+ const result = await mod.checkLicense();
665
+ if (result.valid) {
666
+ console.log(JSON.stringify({}));
667
+ return;
668
+ }
669
+
670
+ // Block via exit code 2 (universal Gemini block) + stderr message.
671
+ process.stderr.write(result.reason || 'License invalid');
672
+ process.exit(2);
673
+ } catch {
674
+ // Fail open on any parse/import error — never block on bugs.
675
+ console.log(JSON.stringify({}));
676
+ }
677
+ }
678
+
679
+ main();
680
+ `;
681
+ }
682
+
625
683
  /**
626
684
  * Generate .gemini/settings.json content with hook configuration.
627
685
  *
@@ -1,4 +1,4 @@
1
- import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync } from 'fs';
1
+ import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
@@ -6,6 +6,7 @@ import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
6
6
  import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
7
7
  import { generateContextFiles } from '../config/context-file-generator.js';
8
8
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
9
+ import { generateClaudeSettings } from '../config/claude-settings-generator.js';
9
10
  import { generateProviderOverlays } from './provider-overlay.js';
10
11
  import { verifyManifest } from './manifest.js';
11
12
 
@@ -146,144 +147,103 @@ export async function installFramework(config) {
146
147
  }
147
148
 
148
149
  /**
149
- * Copy framework files from the Chati.dev source directory
150
+ * Framework directories to copy recursively from source to target.
151
+ * This list controls WHICH top-level directories under chati.dev/ get copied.
152
+ * Everything inside each directory is copied automatically — no need to
153
+ * enumerate individual files.
154
+ *
155
+ * New files added to the source framework are picked up automatically.
150
156
  */
151
- export function copyFrameworkFiles(destDir, provider = 'claude') {
152
- if (!existsSync(FRAMEWORK_SOURCE)) return;
153
-
154
- const filesToCopy = [
155
- 'constitution.md',
156
- 'orchestrator/chati.md',
157
- // DISCOVER agents
158
- 'agents/discover/greenfield-wu.md',
159
- 'agents/discover/brownfield-wu.md',
160
- 'agents/discover/brief.md',
161
- // PLAN agents
162
- 'agents/plan/detail.md',
163
- 'agents/plan/architect.md',
164
- 'agents/plan/architect-system.md',
165
- 'agents/plan/architect-data-engineer.md',
166
- 'agents/plan/ux.md',
167
- 'agents/plan/ux-brand-architect.md',
168
- 'agents/plan/ux-researcher.md',
169
- 'agents/plan/ux-component-engineer.md',
170
- 'agents/plan/phases.md',
171
- 'agents/plan/tasks.md',
172
- // Quality agents
173
- 'agents/quality/qa-planning.md',
174
- 'agents/quality/qa-implementation.md',
175
- // BUILD + DEPLOY agents
176
- 'agents/build/dev.md',
177
- 'agents/deploy/devops.md',
178
- // Templates
179
- 'templates/prd-tmpl.yaml',
180
- 'templates/brownfield-prd-tmpl.yaml',
181
- 'templates/fullstack-architecture-tmpl.yaml',
182
- 'templates/task-tmpl.yaml',
183
- 'templates/qa-gate-tmpl.yaml',
184
- 'templates/quick-brief-tmpl.yaml',
185
- 'templates/brandbook-tmpl.yaml',
186
- 'templates/brandbook-html-tmpl.md',
187
- 'templates/design-token-tmpl.yaml',
188
- 'templates/component-spec-tmpl.yaml',
189
- 'templates/icon-system-tmpl.yaml',
190
- 'templates/session-memory-tmpl.yaml',
191
- // Workflows
192
- 'workflows/greenfield-fullstack.yaml',
193
- 'workflows/brownfield-fullstack.yaml',
194
- 'workflows/brownfield-discovery.yaml',
195
- 'workflows/brownfield-service.yaml',
196
- 'workflows/brownfield-ui.yaml',
197
- 'workflows/quick-flow.yaml',
198
- 'workflows/standard-flow.yaml',
199
- // Quality gates
200
- 'quality-gates/planning-gate.md',
201
- 'quality-gates/implementation-gate.md',
202
- // Schemas
203
- 'schemas/session.schema.json',
204
- 'schemas/config.schema.json',
205
- 'schemas/task.schema.json',
206
- 'schemas/context.schema.json',
207
- 'schemas/memory.schema.json',
208
- // Frameworks
209
- 'frameworks/quality-dimensions.yaml',
210
- 'frameworks/decision-heuristics.yaml',
211
- // Intelligence
212
- 'intelligence/gotchas.yaml',
213
- 'intelligence/patterns.yaml',
214
- 'intelligence/confidence.yaml',
215
- 'intelligence/context-engine.md',
216
- 'intelligence/memory-layer.md',
217
- 'intelligence/decision-engine.md',
218
- // Patterns
219
- 'patterns/elicitation.md',
220
- 'patterns/elicitation-library.yaml',
221
- // Hooks
222
- 'hooks/prism-engine.js',
223
- 'hooks/mode-governance.js',
224
- 'hooks/constitution-guard.js',
225
- 'hooks/session-digest.js',
226
- 'hooks/model-governance.js',
227
- 'hooks/settings.json',
228
- 'hooks/read-protection.js',
229
- // Domains (PRISM Context Engine)
230
- 'domains/constitution.yaml',
231
- 'domains/global.yaml',
232
- 'domains/agents/orchestrator.yaml',
233
- 'domains/agents/greenfield-wu.yaml',
234
- 'domains/agents/brownfield-wu.yaml',
235
- 'domains/agents/brief.yaml',
236
- 'domains/agents/detail.yaml',
237
- 'domains/agents/architect.yaml',
238
- 'domains/agents/ux.yaml',
239
- 'domains/agents/phases.yaml',
240
- 'domains/agents/tasks.yaml',
241
- 'domains/agents/qa-planning.yaml',
242
- 'domains/agents/qa-implementation.yaml',
243
- 'domains/agents/dev.yaml',
244
- 'domains/agents/devops.yaml',
245
- 'domains/workflows/greenfield-fullstack.yaml',
246
- 'domains/workflows/brownfield-fullstack.yaml',
247
- 'domains/workflows/brownfield-discovery.yaml',
248
- 'domains/workflows/brownfield-service.yaml',
249
- 'domains/workflows/brownfield-ui.yaml',
250
- 'domains/workflows/quick-flow.yaml',
251
- 'domains/workflows/standard-flow.yaml',
252
- // i18n
253
- 'i18n/en.yaml',
254
- 'i18n/pt.yaml',
255
- 'i18n/es.yaml',
256
- 'i18n/fr.yaml',
257
- // Migrations
258
- 'migrations/v1.0-to-v1.1.yaml',
259
- // Data
260
- 'data/entity-registry.yaml',
261
- // Context (@ import chain for CLAUDE.md)
262
- 'context/root.md',
263
- 'context/governance.md',
264
- 'context/protocols.md',
265
- 'context/quality.md',
266
- ];
267
-
268
- for (const file of filesToCopy) {
269
- const src = join(FRAMEWORK_SOURCE, file);
270
- const dest = join(destDir, file);
157
+ const FRAMEWORK_DIRS_TO_COPY = [
158
+ 'orchestrator',
159
+ 'agents',
160
+ 'templates',
161
+ 'workflows',
162
+ 'quality-gates',
163
+ 'schemas',
164
+ 'frameworks',
165
+ 'intelligence',
166
+ 'patterns',
167
+ 'hooks',
168
+ 'domains',
169
+ 'i18n',
170
+ 'migrations',
171
+ 'data',
172
+ 'context',
173
+ 'tasks',
174
+ 'presets',
175
+ ];
271
176
 
272
- if (existsSync(src)) {
273
- createDir(dirname(dest));
177
+ /**
178
+ * Loose files at the root of chati.dev/ that get copied directly.
179
+ */
180
+ const FRAMEWORK_ROOT_FILES = [
181
+ 'constitution.md',
182
+ ];
274
183
 
275
- if (provider !== 'claude' && ADAPTABLE_FILES.has(file)) {
184
+ /**
185
+ * Recursively copy a directory from source to destination, applying provider
186
+ * adaptation for files listed in ADAPTABLE_FILES when the provider is not Claude.
187
+ */
188
+ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
189
+ if (!existsSync(srcDir)) return;
190
+ createDir(destDir);
191
+
192
+ const entries = readdirSync(srcDir, { withFileTypes: true });
193
+ for (const entry of entries) {
194
+ const srcPath = join(srcDir, entry.name);
195
+ const destPath = join(destDir, entry.name);
196
+ const relPath = relBase ? join(relBase, entry.name) : entry.name;
197
+
198
+ if (entry.isDirectory()) {
199
+ copyDirRecursive(srcPath, destPath, provider, relPath);
200
+ } else if (entry.isFile()) {
201
+ if (provider !== 'claude' && ADAPTABLE_FILES.has(relPath)) {
276
202
  // Non-Claude provider: read, adapt, write
277
- const content = readFileSync(src, 'utf-8');
278
- writeFileSync(dest, adaptFrameworkFile(content, file, provider), 'utf-8');
203
+ const content = readFileSync(srcPath, 'utf-8');
204
+ writeFileSync(destPath, adaptFrameworkFile(content, relPath, provider), 'utf-8');
279
205
  } else {
280
206
  // Claude or non-adaptable: direct copy
281
- copyFileSync(src, dest);
207
+ copyFileSync(srcPath, destPath);
282
208
  }
283
209
  }
284
210
  }
285
211
  }
286
212
 
213
+ /**
214
+ * Copy framework files from the Chati.dev source directory.
215
+ *
216
+ * Uses recursive directory copy instead of a hardcoded file list, so that
217
+ * new files added to the framework source are automatically picked up.
218
+ *
219
+ * @param {string} destDir - Target framework directory (e.g., {project}/chati.dev/)
220
+ * @param {string} provider - Primary provider for text adaptation ('claude' | 'gemini' | 'codex')
221
+ */
222
+ export function copyFrameworkFiles(destDir, provider = 'claude') {
223
+ if (!existsSync(FRAMEWORK_SOURCE)) return;
224
+
225
+ // Copy loose root files
226
+ for (const file of FRAMEWORK_ROOT_FILES) {
227
+ const src = join(FRAMEWORK_SOURCE, file);
228
+ if (!existsSync(src)) continue;
229
+ const dest = join(destDir, file);
230
+ createDir(dirname(dest));
231
+ if (provider !== 'claude' && ADAPTABLE_FILES.has(file)) {
232
+ const content = readFileSync(src, 'utf-8');
233
+ writeFileSync(dest, adaptFrameworkFile(content, file, provider), 'utf-8');
234
+ } else {
235
+ copyFileSync(src, dest);
236
+ }
237
+ }
238
+
239
+ // Copy each framework directory recursively
240
+ for (const dir of FRAMEWORK_DIRS_TO_COPY) {
241
+ const srcDir = join(FRAMEWORK_SOURCE, dir);
242
+ const destSubDir = join(destDir, dir);
243
+ copyDirRecursive(srcDir, destSubDir, provider, dir);
244
+ }
245
+ }
246
+
287
247
  /**
288
248
  * Configure a specific IDE
289
249
  */
@@ -331,6 +291,12 @@ Pass through all context: session state, handoffs, artifacts, and user input.
331
291
  `;
332
292
  writeFileSync(join(targetDir, '.claude', 'commands', 'chati.md'), routerContent, 'utf-8');
333
293
 
294
+ // .claude/settings.json — wires the 10 chati.dev hooks into Claude Code's
295
+ // hook system and applies a baseline permission policy. THIS IS THE FILE
296
+ // CLAUDE CODE READS — without it, every hook in chati.dev/hooks/ is dormant.
297
+ // (Pre-v4.2.2 bug: this file was never written, hooks never ran.)
298
+ writeClaudeSettingsWithMerge(join(targetDir, '.claude', 'settings.json'));
299
+
334
300
  // MCP config
335
301
  if (selectedMCPs.length > 0) {
336
302
  const mcpConfig = generateClaudeMCPConfig(selectedMCPs);
@@ -348,10 +314,35 @@ Pass through all context: session state, handoffs, artifacts, and user input.
348
314
  // Session lock override file (equivalent to CLAUDE.local.md)
349
315
  writeFileSync(join(targetDir, 'AGENTS.override.md'), generateAgentsOverrideMd(), 'utf-8');
350
316
 
351
- // Starlark execution policies (equivalent to Claude hooks for constitution guard + read protection)
317
+ // Starlark execution policies (Codex sandboxed rule engine)
352
318
  createDir(join(targetDir, '.codex', 'rules'));
353
319
  writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
354
320
  writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
321
+
322
+ // .codex/hooks.json — license-guard wired to UserPromptSubmit (per-turn enforcement).
323
+ // Codex hooks.json is experimental (requires [features] codex_hooks = true in
324
+ // codex config). Without it, license enforcement only happens at slash command
325
+ // entry — a long-running terminal session would not be re-validated.
326
+ writeFileSync(
327
+ join(targetDir, '.codex', 'hooks.json'),
328
+ JSON.stringify({
329
+ $comment: 'chati.dev v4.2.2 — experimental Codex hooks. Requires [features] codex_hooks = true.',
330
+ hooks: {
331
+ UserPromptSubmit: [
332
+ {
333
+ matcher: '.*',
334
+ hooks: [
335
+ {
336
+ type: 'command',
337
+ command: 'node chati.dev/hooks/license-guard.js',
338
+ },
339
+ ],
340
+ },
341
+ ],
342
+ },
343
+ }, null, 2) + '\n',
344
+ 'utf-8'
345
+ );
355
346
  } else if (ideKey === 'gemini-cli') {
356
347
  // Gemini CLI: TOML command file (native format for /chati command)
357
348
  writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter({ orchestratorPath }), 'utf-8');
@@ -471,3 +462,106 @@ function createDir(dir) {
471
462
  mkdirSync(dir, { recursive: true });
472
463
  }
473
464
  }
465
+
466
+ /**
467
+ * Write .claude/settings.json with safe merge against existing user content.
468
+ *
469
+ * If the file does not exist: write fresh chati settings.
470
+ * If the file exists and is valid JSON: merge — chati hooks/permissions are
471
+ * UNIONED into existing values rather than replacing them. User customizations
472
+ * (env, statusLine, plugins, language, alwaysThinkingEnabled, etc.) are preserved.
473
+ * If the file exists but is malformed: leave it alone, log a warning. We will
474
+ * not silently destroy user data.
475
+ */
476
+ function writeClaudeSettingsWithMerge(settingsPath) {
477
+ createDir(dirname(settingsPath));
478
+ const fresh = JSON.parse(generateClaudeSettings());
479
+
480
+ if (!existsSync(settingsPath)) {
481
+ writeFileSync(settingsPath, JSON.stringify(fresh, null, 2) + '\n', 'utf-8');
482
+ return;
483
+ }
484
+
485
+ let existing;
486
+ try {
487
+ existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
488
+ } catch (err) {
489
+ process.stderr.write(`[chati] WARNING: .claude/settings.json exists but is not valid JSON (${err.message}). Skipping merge — chati hooks will NOT be wired. Fix the file and re-run install.\n`);
490
+ return;
491
+ }
492
+
493
+ const merged = mergeClaudeSettings(existing, fresh);
494
+ writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
495
+ }
496
+
497
+ /**
498
+ * Merge two Claude Code settings.json objects.
499
+ *
500
+ * - permissions.allow / permissions.deny: array union (de-duplicated)
501
+ * - permissions.defaultMode: keep existing if set, else use fresh
502
+ * - hooks: per-event union — chati hook commands are appended to existing
503
+ * matcher groups; user's hooks for the same events are preserved
504
+ * - $schema: prefer fresh (always our canonical URL)
505
+ * - all other top-level keys (env, statusLine, plugins, language, etc.):
506
+ * keep existing untouched
507
+ */
508
+ export function mergeClaudeSettings(existing, fresh) {
509
+ const merged = { ...existing };
510
+
511
+ // $schema — chati's canonical URL wins
512
+ if (fresh.$schema) merged.$schema = fresh.$schema;
513
+
514
+ // permissions
515
+ const existingPerms = existing.permissions || {};
516
+ const freshPerms = fresh.permissions || {};
517
+ merged.permissions = {
518
+ ...existingPerms,
519
+ allow: unionArrays(existingPerms.allow, freshPerms.allow),
520
+ deny: unionArrays(existingPerms.deny, freshPerms.deny),
521
+ defaultMode: existingPerms.defaultMode || freshPerms.defaultMode || 'default',
522
+ };
523
+
524
+ // hooks — per-event merge
525
+ const existingHooks = existing.hooks || {};
526
+ const freshHooks = fresh.hooks || {};
527
+ merged.hooks = { ...existingHooks };
528
+
529
+ for (const [eventName, freshGroups] of Object.entries(freshHooks)) {
530
+ const existingGroups = existingHooks[eventName] || [];
531
+ // Append fresh groups; we don't dedup at the group level (different
532
+ // matchers are different groups). Within a single fresh group, the
533
+ // hook commands are unique to chati so duplication risk is minimal.
534
+ // To be safe, dedup by command string across the merged event.
535
+ const seen = new Set();
536
+ const collected = [];
537
+ for (const group of [...existingGroups, ...freshGroups]) {
538
+ const dedupedHooks = (group.hooks || []).filter(h => {
539
+ const key = `${group.matcher || ''}|${h.command}`;
540
+ if (seen.has(key)) return false;
541
+ seen.add(key);
542
+ return true;
543
+ });
544
+ if (dedupedHooks.length > 0) {
545
+ collected.push({
546
+ ...(group.matcher !== undefined ? { matcher: group.matcher } : {}),
547
+ hooks: dedupedHooks,
548
+ });
549
+ }
550
+ }
551
+ merged.hooks[eventName] = collected;
552
+ }
553
+
554
+ return merged;
555
+ }
556
+
557
+ function unionArrays(a, b) {
558
+ const out = [];
559
+ const seen = new Set();
560
+ for (const item of [...(a || []), ...(b || [])]) {
561
+ if (!seen.has(item)) {
562
+ seen.add(item);
563
+ out.push(item);
564
+ }
565
+ }
566
+ return out;
567
+ }
@@ -108,6 +108,44 @@ export function generateConfigYaml(config) {
108
108
  },
109
109
  };
110
110
 
111
+ // Feature flags (Intelligence Upgrade + Article XXI Agent Teams)
112
+ // Must match source chati.dev/config.yaml features block.
113
+ configData.features = {
114
+ hybrid_budget: true,
115
+ anti_dash: true,
116
+ rate_limiter_integration: true,
117
+ l5_keywords: true,
118
+ prompt_size_guard: true,
119
+ ids_decision_engine: true,
120
+ surface_criteria: true,
121
+ parallel_fallback: true,
122
+ tool_mesh: true,
123
+ tech_presets: true,
124
+ doctor_autofix: true,
125
+ brandbook: true,
126
+ // Intelligence Upgrade (v4.0.5 — Claude Code patterns)
127
+ undercover_mode: true,
128
+ memory_extraction: true,
129
+ memory_consolidation: true,
130
+ daily_digest: true,
131
+ structured_session_memory: true,
132
+ static_prism_boundary: true,
133
+ token_bracket_estimation: true,
134
+ model_fallback: true,
135
+ frustration_detection: true,
136
+ bash_security_checks: true,
137
+ // Agent Teams (Article XXI) — default ON in v4.2.2.
138
+ // Only effective when provider === 'claude' (gated in cli.js
139
+ // isAgentTeamsEnabled). Gemini and Codex always fall back to sequential
140
+ // pipeline silently. See plan: was previously false for "safe rollout"
141
+ // but the v4.2.0 launch never actually shipped, so this is a fresh start.
142
+ agent_teams: true,
143
+ team_planning_size: 3,
144
+ team_build_size: 2,
145
+ team_echo_threshold: 0.92,
146
+ team_correction_cycles_max: 2,
147
+ };
148
+
111
149
  // Add telemetry config
112
150
  configData.telemetry = {
113
151
  enabled: config.telemetryEnabled !== false,
@@ -381,24 +381,33 @@ async function handleNext(projectDir) {
381
381
  // Check for parallel group — with Agent Teams override (Article XXI)
382
382
  let action, spawnCommand = null, parallelSpawnCommand = null, parallelAgents = [];
383
383
  let teamData = null;
384
+ const teamsEnabled = isAgentTeamsEnabled(projectDir);
385
+
384
386
  if (nextInfo.isParallel && nextInfo.group.length > 1) {
385
- // When agent_teams is enabled, substitute spawn_parallel with spawn_team
386
- if (isAgentTeamsEnabled(projectDir)) {
387
- const teamType = nextInfo.group.includes('dev') ? 'build' : 'planning';
388
- const teamConfig = TEAM_CONFIGS[teamType];
387
+ // Planning Team auto-spawn: detail + architect + ux are marked parallel: true
388
+ // in agent-selector. When agent_teams enabled, swap spawn_parallel for spawn_team.
389
+ if (teamsEnabled) {
390
+ const teamConfig = TEAM_CONFIGS.planning;
389
391
  const teamId = generateTeamId(teamConfig.slug);
390
392
  action = 'spawn_team';
391
393
  parallelAgents = teamConfig.members;
392
- teamData = {
393
- team_id: teamId,
394
- team_type: teamType,
395
- members: teamConfig.members,
396
- };
394
+ teamData = { team_id: teamId, team_type: 'planning', members: teamConfig.members };
397
395
  } else {
398
396
  action = 'spawn_parallel';
399
397
  parallelAgents = nextInfo.group;
400
398
  parallelSpawnCommand = buildParallelSpawnCommand(nextInfo.group, projectDir, lastAgent, modelInfo.provider, 900000);
401
399
  }
400
+ } else if (nextAgent === 'dev' && teamsEnabled) {
401
+ // Build Team auto-spawn: dev + qa-implementation are NOT parallel in the
402
+ // classic sense (dev writes, qa reviews per-task), so they're not in a
403
+ // parallel group. But under Agent Teams, they coordinate via mailbox + shared
404
+ // task list, which IS the team mechanism. Spawn Build Team here when dev
405
+ // would otherwise spawn solo.
406
+ const teamConfig = TEAM_CONFIGS.build;
407
+ const teamId = generateTeamId(teamConfig.slug);
408
+ action = 'spawn_team';
409
+ parallelAgents = teamConfig.members;
410
+ teamData = { team_id: teamId, team_type: 'build', members: teamConfig.members };
402
411
  } else if (isInteractive) {
403
412
  action = 'activate_interactive';
404
413
  } else {
@@ -978,12 +987,33 @@ function generateTeamId(slug) {
978
987
  return `TM-${date}-${slug}`;
979
988
  }
980
989
 
990
+ /**
991
+ * Agent Teams require BOTH the feature flag AND a Claude provider.
992
+ *
993
+ * Why provider gate: Agent Teams uses Claude Code's native Task tool / subagent
994
+ * spawn API. Gemini and Codex have no equivalent — calling spawn_team for those
995
+ * providers would fail. We always fall back silently to spawn_parallel
996
+ * (Gemini) or spawn_autonomous (Codex) when the active provider is not claude.
997
+ */
981
998
  function isAgentTeamsEnabled(projectDir) {
982
999
  const configPath = join(projectDir, 'chati.dev', 'config.yaml');
983
1000
  if (!existsSync(configPath)) return false;
984
1001
  const raw = readFileSync(configPath, 'utf-8');
985
- const match = raw.match(/agent_teams:\s*(true|false)/);
986
- return match ? match[1] === 'true' : false;
1002
+
1003
+ // Feature flag check
1004
+ const flagMatch = raw.match(/agent_teams:\s*(true|false)/);
1005
+ const flagOn = flagMatch ? flagMatch[1] === 'true' : false;
1006
+ if (!flagOn) return false;
1007
+
1008
+ // Provider gate — Agent Teams is Claude-only.
1009
+ // Read primary_provider from session.yaml (preferred) or active_provider
1010
+ // field. Default to 'claude' if neither exists (early bootstrap).
1011
+ const sessionPath = join(projectDir, '.chati', 'session.yaml');
1012
+ if (!existsSync(sessionPath)) return true; // No session yet — assume claude
1013
+ const sessionRaw = readFileSync(sessionPath, 'utf-8');
1014
+ const providerMatch = sessionRaw.match(/^\s*(?:active_provider|primary_provider):\s*["']?(\w+)/m);
1015
+ const provider = providerMatch ? providerMatch[1] : 'claude';
1016
+ return provider === 'claude';
987
1017
  }
988
1018
 
989
1019
  async function handleSpawnTeam(projectDir, args) {