Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions packages/api/src/config/cat-config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,31 @@ export function getDefaultCatId(): CatId {

// ── Variant CLI effort accessor ──────────────────────────────────────

/**
* Provider-aware effort validation.
* Each CLI has its own valid effort values:
* claude (anthropic): low|medium|high|max
* codex (openai): low|medium|high|xhigh
* gemini (google): low|medium|high|max
* opencode (dare): low|medium|high|max
*/
const VALID_EFFORT_BY_PROVIDER: Record<string, readonly CliEffortLevel[]> = {
anthropic: ['low', 'medium', 'high', 'max'] as const,
openai: ['low', 'medium', 'high', 'xhigh'] as const,
google: ['low', 'medium', 'high', 'max'] as const,
dare: ['low', 'medium', 'high', 'max'] as const,
opencode: ['low', 'medium', 'high', 'max'] as const,
// antigravity, a2a: no effort concept
} as const;

const DEFAULT_EFFORT_BY_PROVIDER: Record<string, CliEffortLevel> = {
anthropic: 'max',
openai: 'xhigh',
google: 'max',
dare: 'max',
opencode: 'max',
};

/** catId → variant index (lazy, rebuilt on config change) */
let _catIdToVariant: Map<string, CatVariant> | null = null;
let _catIdToVariantSource: CatCafeConfig | null = null;
Expand All @@ -676,10 +701,13 @@ export type CliEffortLevel = 'low' | 'medium' | 'high' | 'max' | 'xhigh';

/**
* Get CLI effort level for a cat from cat-config.json.
* Provider-aware normalization: validates effort values against provider CLI spec,
* auto-maps invalid values to provider-correct defaults.
*
* Default when not configured:
* claude (anthropic): 'max'
* codex (openai): 'xhigh'
* others: 'high'
* others: 'max'
*/
export function getCatEffort(catId: string, config?: CatCafeConfig): CliEffortLevel {
const cfg = config ?? getCachedConfig();
Expand All @@ -691,12 +719,29 @@ export function getCatEffort(catId: string, config?: CatCafeConfig): CliEffortLe
}

const variant = _catIdToVariant.get(catId);
if (variant?.cli.effort) return variant.cli.effort;
if (!variant) return 'max';

const provider = variant.provider;
const configuredEffort = variant.cli.effort;

if (configuredEffort) {
// Validate effort against provider's valid values
const validValues = VALID_EFFORT_BY_PROVIDER[provider];
if (validValues && validValues.includes(configuredEffort)) {
return configuredEffort;
}

// Effort value is invalid for this provider — map to default and warn
const defaultValue = DEFAULT_EFFORT_BY_PROVIDER[provider] ?? 'max';
log.warn(
`Invalid effort "${configuredEffort}" for provider "${provider}" (cat: ${catId}). ` +
`Valid values: ${validValues?.join('|') ?? 'unknown'}. Mapped to "${defaultValue}".`,
);
return defaultValue;
}

// Provider-aware defaults
if (variant?.provider === 'openai') return 'xhigh';
if (variant?.provider === 'anthropic') return 'max';
return 'high';
// Provider-aware defaults when not configured
return DEFAULT_EFFORT_BY_PROVIDER[provider] ?? 'max';
}

/** Reset cached config (for testing) */
Expand Down
62 changes: 61 additions & 1 deletion packages/api/src/config/governance/governance-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
* and bootstrap reporting.
*/

import { existsSync } from 'node:fs';
import { lstat, mkdir, readFile, readlink, symlink, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve, sep } from 'node:path';
import type { BootstrapAction, BootstrapReport } from '@cat-cafe/shared';
import { pathsEqual } from '../../utils/project-path.js';
import { bootstrapCatCatalog } from '../cat-catalog-store.js';
import { migrateProviderProfilesToAccounts } from '../migrate-provider-profiles.js';
import type { Provider } from './governance-pack.js';
import {
computePackChecksum,
Expand Down Expand Up @@ -89,7 +92,13 @@ export class GovernanceBootstrapService {
actions.push(action);
}

// 4. Save bootstrap report
// 4. Runtime catalog + account migration for external project invocation.
// Without this, bound accountRef (e.g. my-glm) can resolve in Cat Cafe root
// but fail in external project threads that only have governance files.
const runtimeCatalogActions = this.bootstrapRuntimeCatalogAndAccounts(targetProject, opts.dryRun);
actions.push(...runtimeCatalogActions);

// 5. Save bootstrap report
const report: BootstrapReport = {
projectPath: targetProject,
timestamp: Date.now(),
Expand All @@ -111,6 +120,57 @@ export class GovernanceBootstrapService {
return report;
}

private bootstrapRuntimeCatalogAndAccounts(targetProject: string, dryRun: boolean): BootstrapAction[] {
const templatePath = resolve(this.catCafeRoot, 'cat-template.json');
const catalogRelPath = '.cat-cafe/cat-catalog.json';
const migrationRelPath = '.cat-cafe/accounts-migration';
const catalogPath = resolve(targetProject, catalogRelPath);

if (!existsSync(templatePath)) {
return [
{
file: catalogRelPath,
action: 'skipped',
reason: 'cat-template.json missing in cat-cafe root',
},
];
}

const catalogExists = existsSync(catalogPath);
const catalogAction: BootstrapAction = {
file: catalogRelPath,
action: catalogExists ? 'skipped' : 'created',
reason: catalogExists ? 'runtime catalog already exists' : 'runtime catalog bootstrapped from cat-template.json',
};

if (dryRun) {
return [
catalogAction,
{
file: migrationRelPath,
action: 'skipped',
reason: 'dry-run: account migration not executed',
},
];
}

bootstrapCatCatalog(targetProject, templatePath);
const migration = migrateProviderProfilesToAccounts(targetProject);
const migrationAction: BootstrapAction = migration.migrated
? {
file: migrationRelPath,
action: 'updated',
reason: `migrated ${migration.accountsMigrated ?? 0} account(s), ${migration.credentialsMigrated ?? 0} credential(s)`,
}
: {
file: migrationRelPath,
action: 'skipped',
reason: `migration skipped (${migration.reason ?? 'unknown'})`,
};

return [catalogAction, migrationAction];
}

private async writeManagedBlock(
targetProject: string,
provider: Provider,
Expand Down
Loading
Loading