chati-dev 4.5.6 → 4.5.8

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 (45) hide show
  1. package/framework/agents/build/dev.md +1 -1
  2. package/framework/agents/deploy/devops.md +1 -1
  3. package/framework/agents/discover/brief.md +1 -1
  4. package/framework/agents/discover/brownfield-wu.md +1 -1
  5. package/framework/agents/discover/greenfield-wu.md +1 -1
  6. package/framework/agents/plan/architect-data-engineer.md +1 -1
  7. package/framework/agents/plan/architect-system.md +1 -1
  8. package/framework/agents/plan/architect.md +1 -1
  9. package/framework/agents/plan/detail.md +1 -1
  10. package/framework/agents/plan/phases.md +1 -1
  11. package/framework/agents/plan/tasks.md +1 -1
  12. package/framework/agents/plan/ux-brand-architect.md +1 -1
  13. package/framework/agents/plan/ux-component-engineer.md +1 -1
  14. package/framework/agents/plan/ux-researcher.md +1 -1
  15. package/framework/agents/plan/ux.md +1 -1
  16. package/framework/agents/quality/qa-implementation.md +1 -1
  17. package/framework/agents/quality/qa-planning.md +1 -1
  18. package/framework/agents/quality/qa-visual.md +1 -1
  19. package/framework/agents/shared/visualizer.md +1 -1
  20. package/framework/config.yaml +2 -2
  21. package/framework/context/root.md +1 -1
  22. package/framework/data/entity-registry.yaml +1 -1
  23. package/framework/hooks/license-guard.js +19 -0
  24. package/framework/manifest.json +55 -50
  25. package/framework/manifest.sig +1 -1
  26. package/framework/orchestrator/chati-router.js +177 -71
  27. package/framework/orchestrator/chati.md +1 -1
  28. package/framework/package.json +3 -0
  29. package/framework/schemas/session.schema.json +26 -9
  30. package/package.json +2 -1
  31. package/src/config/claude-settings-generator.js +6 -6
  32. package/src/installer/core.js +55 -25
  33. package/src/installer/path-replacement.js +13 -0
  34. package/src/installer/provider-overlay.js +3 -1
  35. package/src/installer-v2/index.js +55 -0
  36. package/src/orchestrator/cli.js +9 -6
  37. package/src/orchestrator/session-manager.js +134 -96
  38. package/src/terminal/adapters/claude-adapter.js +9 -2
  39. package/src/terminal/adapters/codex-adapter.js +9 -2
  40. package/src/terminal/adapters/gemini-adapter.js +5 -2
  41. package/src/terminal/adapters/grok-adapter.js +13 -3
  42. package/src/terminal/cli-registry.js +5 -0
  43. package/src/terminal/run-agent.js +5 -0
  44. package/src/terminal/run-parallel.js +7 -0
  45. package/src/terminal/spawner.js +49 -10
@@ -35,6 +35,54 @@ function assertString(value, code, label) {
35
35
  if (typeof value !== 'string' || value.trim() === '') throw new ContractError(code, `${label} must be a non-empty string`);
36
36
  }
37
37
 
38
+ function assertExactKeys(value, allowedKeys, label) {
39
+ assertObject(value, 'INVALID_ARTIFACT', label);
40
+ const unexpected = Object.keys(value).filter((key) => !allowedKeys.has(key));
41
+ if (unexpected.length > 0) {
42
+ throw new ContractError('UNEXPECTED_ARTIFACT_FIELD', `${label} contains unsupported fields: ${unexpected.join(', ')}`);
43
+ }
44
+ }
45
+
46
+ const ARTIFACT_KEYS = new Set([
47
+ 'schema_version', 'artifact_type', 'project_id', 'created_at', 'installation',
48
+ 'capability_snapshot', 'brain_read_capability', 'managed_paths',
49
+ 'installation_artifact_id', 'digest',
50
+ ]);
51
+ const INSTALLATION_KEYS = new Set([
52
+ 'installation_id', 'profile', 'enabled_providers', 'capability_snapshot_ref',
53
+ 'policy_ref', 'primary_binding', 'primary_harness', 'external_integrations',
54
+ ]);
55
+ const BINDING_KEYS = new Set([
56
+ 'provider_id', 'harness_id', 'allowed_models', 'allowed_reasoning_configurations',
57
+ ]);
58
+ const SNAPSHOT_KEYS = new Set(['snapshot_id', 'expires_at', 'models']);
59
+ const MODEL_KEYS = new Set([
60
+ 'provider_id', 'model_id', 'actions', 'adjudication_priority',
61
+ 'highest_reasoning_configuration', 'routing_priority', 'tier', 'natural_role',
62
+ ]);
63
+
64
+ function assertClosedArtifactSchema(artifact) {
65
+ assertExactKeys(artifact, ARTIFACT_KEYS, 'artifact');
66
+ assertExactKeys(artifact.installation, INSTALLATION_KEYS, 'installation');
67
+ for (const binding of artifact.installation.enabled_providers ?? []) {
68
+ assertExactKeys(binding, BINDING_KEYS, 'enabled provider binding');
69
+ }
70
+ if (artifact.installation.primary_binding !== undefined) {
71
+ assertExactKeys(artifact.installation.primary_binding, new Set(['provider_id', 'harness_id']), 'primary_binding');
72
+ }
73
+ if (artifact.installation.external_integrations !== undefined) {
74
+ assertExactKeys(artifact.installation.external_integrations, new Set(['clickup']), 'external_integrations');
75
+ }
76
+ assertExactKeys(artifact.capability_snapshot, SNAPSHOT_KEYS, 'capability_snapshot');
77
+ for (const model of artifact.capability_snapshot.models ?? []) {
78
+ assertExactKeys(model, MODEL_KEYS, 'capability model');
79
+ }
80
+ const brainKeys = artifact.brain_read_capability?.status === 'configured'
81
+ ? new Set(['status', 'mode', 'policy_ref'])
82
+ : new Set(['status']);
83
+ assertExactKeys(artifact.brain_read_capability, brainKeys, 'brain_read_capability');
84
+ }
85
+
38
86
  function readClock(clock) {
39
87
  if (typeof clock !== 'function') throw new ContractError('MISSING_CLOCK', 'a deterministic clock function is required');
40
88
  const result = clock();
@@ -127,6 +175,7 @@ export function doctorV2(artifact, { clock = () => new Date() } = {}) {
127
175
  const checks = [];
128
176
  try {
129
177
  assertObject(artifact, 'INVALID_ARTIFACT', 'artifact');
178
+ assertClosedArtifactSchema(artifact);
130
179
  if (artifact.schema_version !== INSTALLATION_SCHEMA_VERSION || artifact.artifact_type !== 'chati-installation') {
131
180
  throw new ContractError('UNSUPPORTED_INSTALLATION_ARTIFACT', 'expected a chati installation artifact v2');
132
181
  }
@@ -138,6 +187,12 @@ export function doctorV2(artifact, { clock = () => new Date() } = {}) {
138
187
  if (!Array.isArray(artifact.managed_paths) || !artifact.managed_paths.every((path) => ensureRelativePath(path))) {
139
188
  throw new ContractError('INVALID_MANAGED_PATH', 'managed_paths must be valid project-relative paths');
140
189
  }
190
+ if (!artifact.managed_paths.includes(INSTALLATION_ARTIFACT_PATH)) {
191
+ throw new ContractError('INVALID_MANAGED_PATH', `managed_paths must include ${INSTALLATION_ARTIFACT_PATH}`);
192
+ }
193
+ if (new Set(artifact.managed_paths).size !== artifact.managed_paths.length) {
194
+ throw new ContractError('DUPLICATE_MANAGED_PATH', 'managed_paths must be unique');
195
+ }
141
196
  checks.push({ name: 'artifact', status: 'pass', message: 'artifact schema and digest are valid' });
142
197
  checks.push({ name: 'selected-bindings', status: 'pass', message: 'only explicitly selected provider/harness bindings are present' });
143
198
  checks.push({ name: 'brain-capability', status: 'pass', message: 'Brain capability is absent or read-only' });
@@ -548,7 +548,7 @@ function sessionToPipelineState(session, projectDir) {
548
548
  * Build the spawn command string for an autonomous agent.
549
549
  * Exported for testing (path resolution must hold in every install layout).
550
550
  */
551
- export function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false) {
551
+ export function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false, routing = {}) {
552
552
  const parts = [
553
553
  'node', RUNNER('run-agent.js'),
554
554
  '--agent', agent,
@@ -557,6 +557,9 @@ export function buildSpawnCommand(agent, projectDir, previousAgent, provider, ti
557
557
  '--previous-agent', previousAgent || 'none',
558
558
  '--provider', provider || 'claude',
559
559
  ...(model ? ['--model', model] : []),
560
+ ...(routing.provider_id ? ['--provider-id', routing.provider_id] : []),
561
+ ...(routing.reasoning_configuration ? ['--reasoning-configuration', routing.reasoning_configuration] : []),
562
+ ...(routing.catalog_snapshot_ref ? ['--catalog-snapshot-ref', routing.catalog_snapshot_ref] : []),
560
563
  ...(strictProvider ? ['--strict-provider'] : []),
561
564
  '--timeout', String(timeout || 600000),
562
565
  ];
@@ -591,7 +594,7 @@ export function buildRoutedParallelSpawnCommands(agents, projectDir, previousAge
591
594
  provider: modelInfo.provider,
592
595
  model: modelInfo.model,
593
596
  reasoning_configuration: modelInfo.reasoning_configuration,
594
- command: buildSpawnCommand(agent, projectDir, previousAgent, modelInfo.provider, timeout, modelInfo.model, modelInfo.source === 'installation-v2'),
597
+ command: buildSpawnCommand(agent, projectDir, previousAgent, modelInfo.provider, timeout, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo),
595
598
  });
596
599
  });
597
600
  }
@@ -849,7 +852,7 @@ async function _handleNextInner(projectDir) {
849
852
  agent,
850
853
  agent_file: agentFile,
851
854
  phase: agentDef?.phase || session.mode,
852
- spawn_command: buildSpawnCommand(agent, projectDir, session.last_handoff || 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2'),
855
+ spawn_command: buildSpawnCommand(agent, projectDir, session.last_handoff || 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo),
853
856
  parallel_spawn_command: null,
854
857
  parallel_agents: [],
855
858
  handoff_status: { valid: true, missing: [], warnings: ['Resuming in-progress agent'] },
@@ -878,7 +881,7 @@ async function _handleNextInner(projectDir) {
878
881
  agent: firstAgent,
879
882
  agent_file: agentFile,
880
883
  phase: 'discover',
881
- spawn_command: buildSpawnCommand(firstAgent, projectDir, 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2'),
884
+ spawn_command: buildSpawnCommand(firstAgent, projectDir, 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo),
882
885
  parallel_spawn_command: null,
883
886
  parallel_agents: [],
884
887
  handoff_status: { valid: true, missing: [], warnings: [] },
@@ -979,10 +982,10 @@ async function _handleNextInner(projectDir) {
979
982
  teamData = { team_id: teamId, team_type: 'build', members: teamConfig.members };
980
983
  } else if (isInteractive) {
981
984
  action = 'spawn_routed_interactive';
982
- spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
985
+ spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo);
983
986
  } else {
984
987
  action = 'spawn_autonomous';
985
- spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
988
+ spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo);
986
989
  }
987
990
 
988
991
  // Surface criteria (Article XVII): even in autonomous mode, a high-stakes
@@ -7,7 +7,8 @@
7
7
 
8
8
  import * as yaml from 'js-yaml';
9
9
  import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
10
- import { join, dirname } from 'path';
10
+ import { join, dirname, resolve } from 'path';
11
+ import lockfile from 'proper-lockfile';
11
12
 
12
13
  const SESSION_FILE = '.chati/session.yaml';
13
14
 
@@ -24,6 +25,35 @@ function writeFileAtomic(path, content, encoding = 'utf-8') {
24
25
  renameSync(tmp, path);
25
26
  }
26
27
 
28
+ function waitForSessionWriteLock(sessionPath, maxWaitMs = 2000) {
29
+ const startedAt = Date.now();
30
+ while (true) {
31
+ try {
32
+ return lockfile.lockSync(sessionPath, {
33
+ realpath: false,
34
+ stale: 30_000,
35
+ update: 10_000,
36
+ });
37
+ } catch (err) {
38
+ if (err.code !== 'ELOCKED') throw err;
39
+ if (Date.now() - startedAt >= maxWaitMs) {
40
+ throw new Error(`Timed out waiting for session write lock after ${maxWaitMs}ms`, { cause: err });
41
+ }
42
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
43
+ }
44
+ }
45
+ }
46
+
47
+ function findSessionProjectDir(startDir) {
48
+ let current = resolve(startDir);
49
+ while (true) {
50
+ if (existsSync(join(current, SESSION_FILE))) return current;
51
+ const parent = dirname(current);
52
+ if (parent === current) return null;
53
+ current = parent;
54
+ }
55
+ }
56
+
27
57
  /**
28
58
  * Default session template.
29
59
  */
@@ -182,8 +212,31 @@ export function migrateSession(session) {
182
212
  return { migrated: false, fromVersion: null, toVersion: CURRENT_SCHEMA_VERSION };
183
213
  }
184
214
 
215
+ let normalizedLegacySelections = false;
216
+ if (Array.isArray(session.model_selections)) {
217
+ session.model_selections = session.model_selections.map((entry) => {
218
+ if (!entry || typeof entry !== 'object') return entry;
219
+ if (entry.task_id && entry.provider && entry.model) return entry;
220
+ const model = entry.actual || entry.recommended;
221
+ if (!entry.agent || !model || !entry.timestamp) return entry;
222
+ const provider = entry.provider || 'claude';
223
+ if (!['claude', 'codex', 'grok', 'gemini'].includes(provider)) return entry;
224
+ normalizedLegacySelections = true;
225
+ return {
226
+ ...entry,
227
+ task_id: entry.task_id || `${entry.agent}-legacy`,
228
+ provider,
229
+ provider_id: entry.provider_id || null,
230
+ model,
231
+ reasoning_configuration: entry.reasoning_configuration || null,
232
+ catalog_snapshot_ref: entry.catalog_snapshot_ref || null,
233
+ status: entry.status || 'dispatched',
234
+ };
235
+ });
236
+ }
237
+
185
238
  if (session.schema_version === CURRENT_SCHEMA_VERSION) {
186
- return { migrated: false, fromVersion: session.schema_version, toVersion: CURRENT_SCHEMA_VERSION };
239
+ return { migrated: normalizedLegacySelections, fromVersion: session.schema_version, toVersion: CURRENT_SCHEMA_VERSION };
187
240
  }
188
241
 
189
242
  // v0 (no schema_version) → v1.0 → v1.1 → v1.2
@@ -294,22 +347,22 @@ export function loadSession(projectDir) {
294
347
 
295
348
  try {
296
349
  const content = readFileSync(sessionPath, 'utf-8');
297
- const session = yaml.load(content);
350
+ let session = yaml.load(content);
298
351
 
299
352
  // Run migration if needed
300
353
  const migration = migrateSession(session);
301
354
  if (migration.migrated) {
302
- try {
303
- const yamlContent = yaml.dump(session, { lineWidth: -1, noRefs: true });
304
- writeFileAtomic(sessionPath, yamlContent);
305
- } catch (writeErr) {
355
+ const persisted = mutateSession(projectDir, current => current);
356
+ if (persisted.saved) {
357
+ session = persisted.session;
358
+ } else {
306
359
  // Migration write-back failed (read-only filesystem, EACCES). The
307
360
  // session continues with the migrated in-memory copy, but disk stays
308
361
  // on the old schema and re-migrates on every load. Surface it so the
309
362
  // user can fix the cause (make .chati/ writable) instead of it failing
310
363
  // silently. The production path (writable FS) never reaches here.
311
364
  process.stderr.write(
312
- `[chati] warning: session migrated in memory but could not be written back to ${sessionPath} (${writeErr.code || writeErr.message}); it will re-migrate on every load until the directory is writable\n`
365
+ `[chati] warning: session migrated in memory but could not be written back to ${sessionPath} (${persisted.error}); it will re-migrate on every load until the directory is writable\n`
313
366
  );
314
367
  }
315
368
  }
@@ -336,115 +389,108 @@ export function loadSession(projectDir) {
336
389
  * @returns {{ saved: boolean }}
337
390
  */
338
391
  export function updateSession(projectDir, updates) {
339
- const sessionPath = join(projectDir, SESSION_FILE);
340
-
341
- // Load existing session
342
- const loadResult = loadSession(projectDir);
343
-
344
- if (!loadResult.loaded) {
345
- return {
346
- saved: false,
347
- error: loadResult.error,
348
- };
349
- }
350
-
351
- try {
392
+ return mutateSession(projectDir, (session) => {
352
393
  // Merge updates
353
394
  const merged = {
354
- ...loadResult.session,
395
+ ...session,
355
396
  ...updates,
356
397
  };
357
398
 
358
399
  // Deep merge agents if provided
359
400
  if (updates.agents) {
360
401
  merged.agents = {
361
- ...loadResult.session.agents,
402
+ ...session.agents,
362
403
  ...updates.agents,
363
404
  };
364
405
 
365
406
  // Merge individual agent data
366
407
  for (const [agentName, agentData] of Object.entries(updates.agents)) {
367
408
  merged.agents[agentName] = {
368
- ...loadResult.session.agents[agentName],
409
+ ...session.agents[agentName],
369
410
  ...agentData,
370
411
  };
371
412
  }
372
413
  }
414
+ return merged;
415
+ });
416
+ }
373
417
 
374
- // Write back
375
- const yamlContent = yaml.dump(merged, {
376
- lineWidth: -1,
377
- noRefs: true,
378
- });
379
-
418
+ function mutateSession(projectDir, mutator) {
419
+ const sessionPath = join(projectDir, SESSION_FILE);
420
+ if (!existsSync(sessionPath)) return { saved: false, error: 'Session file not found' };
421
+ let releaseLock = null;
422
+ let outcome;
423
+ try {
424
+ releaseLock = waitForSessionWriteLock(sessionPath);
425
+ const session = yaml.load(readFileSync(sessionPath, 'utf-8'));
426
+ if (!session || typeof session !== 'object' || Array.isArray(session)) {
427
+ throw new Error('Invalid session document');
428
+ }
429
+ migrateSession(session);
430
+ const nextSession = mutator(session) || session;
431
+ const yamlContent = yaml.dump(nextSession, { lineWidth: -1, noRefs: true });
380
432
  writeFileAtomic(sessionPath, yamlContent);
381
-
382
- return {
383
- saved: true,
384
- session: merged,
385
- };
433
+ outcome = { saved: true, session: nextSession };
386
434
  } catch (err) {
387
- return {
388
- saved: false,
389
- error: `Failed to update session: ${err.message}`,
390
- };
435
+ outcome = { saved: false, error: `Failed to update session: ${err.message}` };
436
+ } finally {
437
+ if (releaseLock) {
438
+ try {
439
+ releaseLock();
440
+ } catch (err) {
441
+ outcome = { saved: false, error: `Session write lock was compromised: ${err.message}` };
442
+ }
443
+ }
391
444
  }
445
+ return outcome;
392
446
  }
393
447
 
394
448
  /**
395
- * Record a mode transition in session history.
396
- * @param {string} projectDir
397
- * @param {object} transition - { from, to, trigger, reason }
398
- * @returns {{ saved: boolean }}
449
+ * Append the exact provider/model binding used for an execution.
450
+ * A short cross-process lock prevents parallel agents from overwriting each
451
+ * other's append-only audit entries.
399
452
  */
400
- export function recordModeTransition(projectDir, transition) {
401
- const loadResult = loadSession(projectDir);
402
-
403
- if (!loadResult.loaded) {
404
- return {
405
- saved: false,
406
- error: loadResult.error,
407
- };
408
- }
409
-
410
- const session = loadResult.session;
411
-
412
- // Add transition to history
413
- session.mode_transitions = session.mode_transitions || [];
414
- session.mode_transitions.push({
415
- from: transition.from,
416
- to: transition.to,
417
- trigger: transition.trigger || 'manual',
418
- reason: transition.reason || '',
419
- timestamp: new Date().toISOString(),
453
+ export function recordModelSelection(projectDir, selection) {
454
+ const sessionProjectDir = findSessionProjectDir(projectDir);
455
+ if (!sessionProjectDir) return { saved: false, error: 'Session file not found in working directory or its parents' };
456
+
457
+ return mutateSession(sessionProjectDir, (session) => {
458
+ session.model_selections = Array.isArray(session.model_selections) ? session.model_selections : [];
459
+ session.model_selections.push({
460
+ agent: selection.agent,
461
+ task_id: selection.taskId,
462
+ provider: selection.provider,
463
+ provider_id: selection.providerId || null,
464
+ model: selection.model,
465
+ reasoning_configuration: selection.reasoningConfiguration || null,
466
+ catalog_snapshot_ref: selection.catalogSnapshotRef || null,
467
+ status: selection.status || 'dispatched',
468
+ timestamp: selection.timestamp || new Date().toISOString(),
469
+ });
470
+ return session;
420
471
  });
421
-
422
- // Update current mode (both flat and nested for compatibility)
423
- session.mode = transition.to;
424
- if (session.project) {
425
- session.project.state = transition.to;
426
- }
427
-
428
- return writeSessionToDisk(projectDir, session);
429
472
  }
430
473
 
431
474
  /**
432
- * Persist the full session object to disk. Avoids routing through updateSession's
433
- * spread+deep-merge logic (which can corrupt list fields like `completed_agents`
434
- * when the full session is passed as the updates arg).
475
+ * Record a mode transition in session history.
435
476
  * @param {string} projectDir
436
- * @param {object} session
437
- * @returns {{ saved: boolean, session?: object, error?: string }}
477
+ * @param {object} transition - { from, to, trigger, reason }
478
+ * @returns {{ saved: boolean }}
438
479
  */
439
- function writeSessionToDisk(projectDir, session) {
440
- const sessionPath = join(projectDir, SESSION_FILE);
441
- try {
442
- const yamlContent = yaml.dump(session, { lineWidth: -1, noRefs: true });
443
- writeFileAtomic(sessionPath, yamlContent);
444
- return { saved: true, session };
445
- } catch (err) {
446
- return { saved: false, error: `Failed to write session: ${err.message}` };
447
- }
480
+ export function recordModeTransition(projectDir, transition) {
481
+ return mutateSession(projectDir, (session) => {
482
+ session.mode_transitions = session.mode_transitions || [];
483
+ session.mode_transitions.push({
484
+ from: transition.from,
485
+ to: transition.to,
486
+ trigger: transition.trigger || 'manual',
487
+ reason: transition.reason || '',
488
+ timestamp: new Date().toISOString(),
489
+ });
490
+ session.mode = transition.to;
491
+ if (session.project) session.project.state = transition.to;
492
+ return session;
493
+ });
448
494
  }
449
495
 
450
496
  /**
@@ -457,17 +503,8 @@ function writeSessionToDisk(projectDir, session) {
457
503
  * @returns {{ saved: boolean }}
458
504
  */
459
505
  export function recordAgentCompletion(projectDir, completion) {
460
- const loadResult = loadSession(projectDir);
461
-
462
- if (!loadResult.loaded) {
463
- return {
464
- saved: false,
465
- error: loadResult.error,
466
- };
467
- }
468
-
469
- const session = loadResult.session;
470
506
  const { agent, status, score, outputs = [], handoffData = {} } = completion;
507
+ return mutateSession(projectDir, (session) => {
471
508
 
472
509
  // Update agent data
473
510
  if (!session.agents[agent]) {
@@ -547,7 +584,8 @@ export function recordAgentCompletion(projectDir, completion) {
547
584
  }
548
585
  }
549
586
 
550
- return writeSessionToDisk(projectDir, session);
587
+ return session;
588
+ });
551
589
  }
552
590
 
553
591
  /**
@@ -19,15 +19,22 @@
19
19
  */
20
20
  export function buildCommand(config, provider) {
21
21
  const args = [...provider.baseArgs];
22
+ let effectiveModel = null;
22
23
 
23
24
  if (config.model) {
24
- const resolvedModel = provider.modelMap[config.model] || config.model;
25
- args.push(provider.modelFlag, resolvedModel);
25
+ effectiveModel = provider.modelMap[config.model] || config.model;
26
+ args.push(provider.modelFlag, effectiveModel);
27
+ }
28
+
29
+ if (config.reasoningConfiguration) {
30
+ args.push('--effort', config.reasoningConfiguration);
26
31
  }
27
32
 
28
33
  return {
29
34
  command: provider.command,
30
35
  args,
31
36
  stdinPrompt: config.prompt || null,
37
+ effectiveModel,
38
+ effectiveReasoningConfiguration: config.reasoningConfiguration || null,
32
39
  };
33
40
  }
@@ -14,10 +14,15 @@
14
14
  */
15
15
  export function buildCommand(config, provider) {
16
16
  const args = [...provider.baseArgs];
17
+ let effectiveModel = null;
17
18
 
18
19
  if (config.model) {
19
- const resolvedModel = provider.modelMap[config.model] || config.model;
20
- args.push(provider.modelFlag, resolvedModel);
20
+ effectiveModel = provider.modelMap[config.model] || config.model;
21
+ args.push(provider.modelFlag, effectiveModel);
22
+ }
23
+
24
+ if (config.reasoningConfiguration) {
25
+ args.push('-c', `model_reasoning_effort=${JSON.stringify(config.reasoningConfiguration)}`);
21
26
  }
22
27
 
23
28
  // Codex exec reads prompt from stdin when `-` is passed
@@ -27,5 +32,7 @@ export function buildCommand(config, provider) {
27
32
  command: provider.command,
28
33
  args,
29
34
  stdinPrompt: config.prompt || null,
35
+ effectiveModel,
36
+ effectiveReasoningConfiguration: config.reasoningConfiguration || null,
30
37
  };
31
38
  }
@@ -14,15 +14,18 @@
14
14
  */
15
15
  export function buildCommand(config, provider) {
16
16
  const args = [...provider.baseArgs];
17
+ let effectiveModel = null;
17
18
 
18
19
  if (config.model) {
19
- const resolvedModel = provider.modelMap[config.model] || config.model;
20
- args.push(provider.modelFlag, resolvedModel);
20
+ effectiveModel = provider.modelMap[config.model] || config.model;
21
+ args.push(provider.modelFlag, effectiveModel);
21
22
  }
22
23
 
23
24
  return {
24
25
  command: provider.command,
25
26
  args,
26
27
  stdinPrompt: config.prompt || null,
28
+ effectiveModel,
29
+ effectiveReasoningConfiguration: null,
27
30
  };
28
31
  }
@@ -7,10 +7,20 @@
7
7
  */
8
8
  export function buildCommand(config, provider) {
9
9
  const args = [...provider.baseArgs];
10
+ let effectiveModel = null;
10
11
  if (config.model) {
11
- const resolvedModel = provider.modelMap[config.model] || config.model;
12
- args.push(provider.modelFlag, resolvedModel);
12
+ effectiveModel = provider.modelMap[config.model] || config.model;
13
+ args.push(provider.modelFlag, effectiveModel);
14
+ }
15
+ if (config.reasoningConfiguration) {
16
+ args.push('--reasoning-effort', config.reasoningConfiguration);
13
17
  }
14
18
  args.push('-p', config.prompt || '');
15
- return { command: provider.command, args, stdinPrompt: null };
19
+ return {
20
+ command: provider.command,
21
+ args,
22
+ stdinPrompt: null,
23
+ effectiveModel,
24
+ effectiveReasoningConfiguration: config.reasoningConfiguration || null,
25
+ };
16
26
  }
@@ -19,6 +19,7 @@ import { parseProviderConfig, parseAgentOverride } from '../utils/config-parser.
19
19
  * @property {string} command - CLI command name
20
20
  * @property {string[]} baseArgs - Default CLI arguments for non-interactive mode
21
21
  * @property {string} modelFlag - CLI flag for model selection
22
+ * @property {string} defaultModel - Explicit legacy default model alias
22
23
  * @property {boolean} stdinSupport - Whether prompts can be piped via stdin
23
24
  * @property {boolean} hooksSupport - Whether the CLI supports hooks (event middleware)
24
25
  * @property {boolean} mcpSupport - Whether the CLI supports MCP servers
@@ -34,6 +35,7 @@ const PROVIDERS = {
34
35
  command: 'claude',
35
36
  baseArgs: ['--print', '--dangerously-skip-permissions'],
36
37
  modelFlag: '--model',
38
+ defaultModel: 'sonnet',
37
39
  stdinSupport: true,
38
40
  hooksSupport: true,
39
41
  mcpSupport: true,
@@ -50,6 +52,7 @@ const PROVIDERS = {
50
52
  command: 'gemini',
51
53
  baseArgs: [],
52
54
  modelFlag: '--model',
55
+ defaultModel: 'pro',
53
56
  stdinSupport: true,
54
57
  hooksSupport: true,
55
58
  mcpSupport: true,
@@ -65,6 +68,7 @@ const PROVIDERS = {
65
68
  command: 'codex',
66
69
  baseArgs: ['exec'],
67
70
  modelFlag: '-m',
71
+ defaultModel: 'codex',
68
72
  stdinSupport: true,
69
73
  hooksSupport: false,
70
74
  mcpSupport: true,
@@ -80,6 +84,7 @@ const PROVIDERS = {
80
84
  command: 'grok',
81
85
  baseArgs: [],
82
86
  modelFlag: '-m',
87
+ defaultModel: 'grok',
83
88
  stdinSupport: false,
84
89
  hooksSupport: true,
85
90
  mcpSupport: true,
@@ -130,6 +130,9 @@ async function main() {
130
130
  workingDir: projectDir,
131
131
  timeout,
132
132
  strictProvider: args['strict-provider'] === 'true',
133
+ providerId: args['provider-id'] || null,
134
+ reasoningConfiguration: args['reasoning-configuration'] || null,
135
+ catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
133
136
  });
134
137
  } catch (err) {
135
138
  outputError(`Failed to spawn terminal: ${err.message}`);
@@ -144,6 +147,7 @@ async function main() {
144
147
  errorType: 'agent_failure',
145
148
  agent: args.agent,
146
149
  provider: promptResult.provider || args.provider || 'claude',
150
+ reasoningConfiguration: args['reasoning-configuration'] || null,
147
151
  phase: sessionState?.phase || 'unknown',
148
152
  });
149
153
  await flushAndSend(projectDir);
@@ -212,6 +216,7 @@ async function main() {
212
216
  agent: args.agent,
213
217
  model: promptResult.model,
214
218
  provider: promptResult.provider || args.provider || 'claude',
219
+ reasoningConfiguration: args['reasoning-configuration'] || null,
215
220
  exitCode: handle.exitCode,
216
221
  handoff: parsed.handoff,
217
222
  elapsed,
@@ -55,6 +55,9 @@ function createSpawnConfig({ agent, taskId, promptResult, projectDir, timeout, s
55
55
  workingDir: projectDir,
56
56
  timeout,
57
57
  strictProvider,
58
+ providerId: promptResult.providerId || null,
59
+ reasoningConfiguration: promptResult.reasoningConfiguration || null,
60
+ catalogSnapshotRef: promptResult.catalogSnapshotRef || null,
58
61
  };
59
62
  }
60
63
 
@@ -120,6 +123,10 @@ async function main() {
120
123
  strictProvider: args['strict-provider'] === 'true',
121
124
  });
122
125
 
126
+ promptResult.providerId = args['provider-id'] || null;
127
+ promptResult.reasoningConfiguration = args['reasoning-configuration'] || null;
128
+ promptResult.catalogSnapshotRef = args['catalog-snapshot-ref'] || null;
129
+
123
130
  configs.push(createSpawnConfig({
124
131
  agent: agents[i], taskId: taskIds[i], promptResult, projectDir, timeout,
125
132
  strictProvider: args['strict-provider'] === 'true',