@open-agent-toolkit/cli 0.1.43 → 0.1.45

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 (47) hide show
  1. package/assets/config/dispatch-matrix-recommendation.json +23 -0
  2. package/assets/docs/cli-utilities/configuration.md +59 -20
  3. package/assets/docs/cli-utilities/workflow-gates.md +79 -16
  4. package/assets/docs/contributing/code.md +10 -4
  5. package/assets/docs/contributing/hooks-and-safety.md +23 -0
  6. package/assets/docs/workflows/projects/dispatch-ceiling.md +80 -10
  7. package/assets/public-package-versions.json +4 -4
  8. package/assets/skills/oat-project-implement/SKILL.md +53 -11
  9. package/assets/skills/oat-project-plan/SKILL.md +22 -3
  10. package/assets/skills/oat-project-quick-start/SKILL.md +22 -3
  11. package/assets/templates/state.md +5 -0
  12. package/dist/commands/config/index.d.ts +6 -0
  13. package/dist/commands/config/index.d.ts.map +1 -1
  14. package/dist/commands/config/index.js +366 -8
  15. package/dist/commands/doctor/index.d.ts +6 -1
  16. package/dist/commands/doctor/index.d.ts.map +1 -1
  17. package/dist/commands/doctor/index.js +138 -4
  18. package/dist/commands/gate/index.d.ts +39 -2
  19. package/dist/commands/gate/index.d.ts.map +1 -1
  20. package/dist/commands/gate/index.js +343 -30
  21. package/dist/commands/internal/cursor-current-target.d.ts +37 -0
  22. package/dist/commands/internal/cursor-current-target.d.ts.map +1 -0
  23. package/dist/commands/internal/cursor-current-target.js +228 -0
  24. package/dist/commands/internal/index.d.ts.map +1 -1
  25. package/dist/commands/internal/index.js +2 -0
  26. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  27. package/dist/commands/project/dispatch-ceiling/index.js +359 -16
  28. package/dist/config/oat-config.d.ts +15 -5
  29. package/dist/config/oat-config.d.ts.map +1 -1
  30. package/dist/config/oat-config.js +103 -10
  31. package/dist/config/resolve.js +12 -0
  32. package/dist/manifest/manifest.types.d.ts +12 -12
  33. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  34. package/dist/providers/ceiling/registry.js +16 -0
  35. package/dist/providers/identity/availability.d.ts +22 -0
  36. package/dist/providers/identity/availability.d.ts.map +1 -0
  37. package/dist/providers/identity/availability.js +119 -0
  38. package/dist/providers/identity/family.d.ts +13 -0
  39. package/dist/providers/identity/family.d.ts.map +1 -0
  40. package/dist/providers/identity/family.js +32 -0
  41. package/dist/providers/identity/provenance.d.ts +21 -0
  42. package/dist/providers/identity/provenance.d.ts.map +1 -0
  43. package/dist/providers/identity/provenance.js +65 -0
  44. package/dist/providers/identity/stamp.d.ts +35 -0
  45. package/dist/providers/identity/stamp.d.ts.map +1 -0
  46. package/dist/providers/identity/stamp.js +171 -0
  47. package/package.json +2 -2
@@ -1,13 +1,24 @@
1
+ import { readFile as readFileDefault } from 'node:fs/promises';
1
2
  import { join } from 'node:path';
2
3
  import { buildCommandContext } from '../../app/command-context.js';
3
4
  import { resolveProjectsRoot } from '../shared/oat-paths.js';
5
+ import { confirmAction, } from '../shared/shared.prompts.js';
4
6
  import { readGlobalOptions } from '../shared/shared.utils.js';
5
7
  import { compileDispatchCeilingPreset } from '../../config/dispatch-ceiling-preset.js';
6
8
  import { readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
7
9
  import { resolveEffectiveConfig, } from '../../config/resolve.js';
10
+ import { resolveAssetsRoot } from '../../fs/assets.js';
8
11
  import { resolveProjectRoot } from '../../fs/paths.js';
12
+ import { validateMatrixCell, } from '../../providers/identity/availability.js';
9
13
  import { Command } from 'commander';
10
14
  import { createConfigDumpCommand } from './dump.js';
15
+ const DISPATCH_CEILING_PROVIDER_KEY_PREFIX = 'workflow.dispatchCeiling.providers.';
16
+ const DISPATCH_MATRIX_TIERS = [
17
+ 'economy',
18
+ 'balanced',
19
+ 'high',
20
+ 'frontier',
21
+ ];
11
22
  const KEY_ORDER = [
12
23
  'activeIdea',
13
24
  'activeProject',
@@ -547,10 +558,49 @@ const DEFAULT_DEPENDENCIES = {
547
558
  writeUserConfig,
548
559
  resolveProjectsRoot,
549
560
  resolveEffectiveConfig,
561
+ resolveAssetsRoot,
562
+ readFile: (path) => readFileDefault(path, 'utf8'),
563
+ confirmAction,
564
+ validateMatrixCell,
550
565
  processEnv: process.env,
551
566
  };
552
567
  function isConfigKey(value) {
553
- return KEY_ORDER.includes(value);
568
+ return (KEY_ORDER.includes(value) ||
569
+ isDispatchCeilingProviderKey(value));
570
+ }
571
+ function isDispatchCeilingProviderKey(value) {
572
+ return (value.startsWith(DISPATCH_CEILING_PROVIDER_KEY_PREFIX) &&
573
+ value.slice(DISPATCH_CEILING_PROVIDER_KEY_PREFIX.length).trim().length > 0);
574
+ }
575
+ function isDispatchMatrixTier(value) {
576
+ return DISPATCH_MATRIX_TIERS.includes(value);
577
+ }
578
+ function parseDispatchCeilingProviderConfigKey(key) {
579
+ const suffix = key.slice(DISPATCH_CEILING_PROVIDER_KEY_PREFIX.length).trim();
580
+ const parts = suffix.split('.').map((part) => part.trim());
581
+ const invalidMessage = `Invalid config key ${key}: expected workflow.dispatchCeiling.providers.<provider> or workflow.dispatchCeiling.providers.<provider>.<tier> with tier one of ${DISPATCH_MATRIX_TIERS.join(' | ')}.`;
582
+ if (parts.some((part) => part.length === 0)) {
583
+ throw new Error(invalidMessage);
584
+ }
585
+ if (parts.length === 1) {
586
+ const provider = parts[0];
587
+ if (!provider) {
588
+ throw new Error(invalidMessage);
589
+ }
590
+ return { provider };
591
+ }
592
+ const tier = parts[parts.length - 1];
593
+ if (!tier || !isDispatchMatrixTier(tier)) {
594
+ throw new Error(invalidMessage);
595
+ }
596
+ const provider = parts.slice(0, -1).join('.').trim();
597
+ if (!provider) {
598
+ throw new Error(invalidMessage);
599
+ }
600
+ return { provider, tier };
601
+ }
602
+ function providerNameFromConfigKey(key) {
603
+ return parseDispatchCeilingProviderConfigKey(key).provider;
554
604
  }
555
605
  function normalizeSharedRoot(value) {
556
606
  const trimmed = value.trim();
@@ -586,6 +636,15 @@ const WORKFLOW_ENUM_VALUES = {
586
636
  'fable',
587
637
  ],
588
638
  };
639
+ function closedDispatchProviderValues(provider) {
640
+ if (provider === 'codex') {
641
+ return WORKFLOW_ENUM_VALUES['workflow.dispatchCeiling.providers.codex'];
642
+ }
643
+ if (provider === 'claude') {
644
+ return WORKFLOW_ENUM_VALUES['workflow.dispatchCeiling.providers.claude'];
645
+ }
646
+ return undefined;
647
+ }
589
648
  const WORKFLOW_BOOLEAN_KEYS = new Set([
590
649
  'workflow.archiveOnComplete',
591
650
  'workflow.createPrOnComplete',
@@ -666,8 +725,95 @@ function parseWorkflowValue(key, rawValue) {
666
725
  }
667
726
  return normalized;
668
727
  }
728
+ if (isDispatchCeilingProviderKey(key)) {
729
+ const { provider } = parseDispatchCeilingProviderConfigKey(key);
730
+ const normalized = rawValue.trim();
731
+ if (normalized.length === 0) {
732
+ throw new Error(`Invalid value for ${key}: provider values cannot be empty`);
733
+ }
734
+ const closedValues = closedDispatchProviderValues(provider);
735
+ if (closedValues && !closedValues.includes(normalized)) {
736
+ throw new Error(`Invalid value for ${key}: expected one of ${closedValues.join(' | ')}, got '${rawValue}'`);
737
+ }
738
+ return normalized;
739
+ }
669
740
  throw new Error(`Unknown workflow key: ${key}`);
670
741
  }
742
+ function dispatchProviderAvailabilityWarning(key, value, availability) {
743
+ return matrixCellAvailabilityWarning(key, value, availability);
744
+ }
745
+ const DISPATCH_MATRIX_RECOMMENDATION_ASSET = join('config', 'dispatch-matrix-recommendation.json');
746
+ function isRecord(value) {
747
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
748
+ }
749
+ function matrixCellAvailabilityWarning(path, value, availability) {
750
+ if (availability === 'valid') {
751
+ return null;
752
+ }
753
+ if (availability === 'unknown-value') {
754
+ return `${path} value '${value}' was not recognized by the provider availability oracle; saving anyway.`;
755
+ }
756
+ return `${path} value '${value}' could not be validated because the provider availability oracle is unavailable; saving anyway.`;
757
+ }
758
+ function parseDispatchMatrixRecommendation(raw) {
759
+ const parsed = JSON.parse(raw);
760
+ if (!isRecord(parsed)) {
761
+ throw new Error('Dispatch matrix recommendation asset must be an object.');
762
+ }
763
+ const version = typeof parsed.version === 'string' ? parsed.version.trim() : '';
764
+ if (!version) {
765
+ throw new Error('Dispatch matrix recommendation asset is missing a version.');
766
+ }
767
+ if (!isRecord(parsed.providers)) {
768
+ throw new Error('Dispatch matrix recommendation asset is missing providers.');
769
+ }
770
+ return {
771
+ version,
772
+ providers: parsed.providers,
773
+ };
774
+ }
775
+ function isRouteTarget(entry) {
776
+ return isRecord(entry);
777
+ }
778
+ function addDispatchMatrixCellRefs(refs, provider, path, cell) {
779
+ if (typeof cell === 'string') {
780
+ refs.push({ provider, value: cell, path });
781
+ return;
782
+ }
783
+ for (const [index, entry] of cell.entries()) {
784
+ const entryPath = `${path}[${index}]`;
785
+ if (typeof entry === 'string') {
786
+ refs.push({ provider, value: entry, path: entryPath });
787
+ continue;
788
+ }
789
+ if (!isRouteTarget(entry)) {
790
+ continue;
791
+ }
792
+ if (entry.model) {
793
+ refs.push({ provider, value: entry.model, path: `${entryPath}.model` });
794
+ }
795
+ if (entry.effort) {
796
+ refs.push({ provider, value: entry.effort, path: `${entryPath}.effort` });
797
+ }
798
+ }
799
+ }
800
+ function collectDispatchMatrixCellRefs(providers) {
801
+ const refs = [];
802
+ for (const [provider, providerValue] of Object.entries(providers)) {
803
+ const providerPath = `workflow.dispatchCeiling.providers.${provider}`;
804
+ if (typeof providerValue === 'string') {
805
+ refs.push({ provider, value: providerValue, path: providerPath });
806
+ continue;
807
+ }
808
+ for (const [tier, cell] of Object.entries(providerValue)) {
809
+ if (cell === undefined) {
810
+ continue;
811
+ }
812
+ addDispatchMatrixCellRefs(refs, provider, `${providerPath}.${tier}`, cell);
813
+ }
814
+ }
815
+ return refs;
816
+ }
671
817
  function applyWorkflowValue(workflow, key, value) {
672
818
  const subKey = key.slice('workflow.'.length);
673
819
  if (subKey === 'dispatchPolicy.mode') {
@@ -714,15 +860,37 @@ function applyWorkflowValue(workflow, key, value) {
714
860
  },
715
861
  };
716
862
  }
717
- if (subKey.startsWith('dispatchCeiling.providers.')) {
718
- const providerKey = subKey.slice('dispatchCeiling.providers.'.length);
863
+ if (isDispatchCeilingProviderKey(key)) {
864
+ const { provider, tier } = parseDispatchCeilingProviderConfigKey(key);
865
+ const providers = workflow.dispatchCeiling?.providers ?? {};
866
+ if (tier) {
867
+ const existingProviderValue = providers[provider];
868
+ const existingTierMap = existingProviderValue &&
869
+ typeof existingProviderValue === 'object' &&
870
+ !Array.isArray(existingProviderValue)
871
+ ? existingProviderValue
872
+ : {};
873
+ return {
874
+ ...workflow,
875
+ dispatchCeiling: {
876
+ ...workflow.dispatchCeiling,
877
+ providers: {
878
+ ...providers,
879
+ [provider]: {
880
+ ...existingTierMap,
881
+ [tier]: value,
882
+ },
883
+ },
884
+ },
885
+ };
886
+ }
719
887
  return {
720
888
  ...workflow,
721
889
  dispatchCeiling: {
722
890
  ...workflow.dispatchCeiling,
723
891
  providers: {
724
- ...workflow.dispatchCeiling?.providers,
725
- [providerKey]: value,
892
+ ...providers,
893
+ [provider]: value,
726
894
  },
727
895
  },
728
896
  };
@@ -769,12 +937,31 @@ async function getConfigValue(repoRoot, userConfigDir, key, dependencies) {
769
937
  source: entry.source,
770
938
  };
771
939
  }
772
- async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, dependencies) {
940
+ async function listConfigKeys(repoRoot, userConfigDir, dependencies) {
941
+ const resolved = await dependencies.resolveEffectiveConfig(repoRoot, userConfigDir, dependencies.processEnv);
942
+ const staticKeys = new Set(KEY_ORDER);
943
+ const dynamicProviderKeys = Object.keys(resolved.resolved)
944
+ .filter(isDispatchCeilingProviderKey)
945
+ .filter((key) => !staticKeys.has(key))
946
+ .sort();
947
+ return [...KEY_ORDER, ...dynamicProviderKeys];
948
+ }
949
+ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, dependencies, warn) {
773
950
  validateSurfaceForKey(key, surface);
774
951
  const effectiveSurface = surface === 'auto' ? defaultSurfaceForKey(key) : surface;
775
952
  if (isWorkflowKey(key)) {
776
953
  const parsedValue = parseWorkflowValue(key, rawValue);
777
954
  const displayValue = typeof parsedValue === 'boolean' ? String(parsedValue) : parsedValue;
955
+ if (typeof parsedValue === 'string' && isDispatchCeilingProviderKey(key)) {
956
+ const availability = await dependencies.validateMatrixCell(providerNameFromConfigKey(key), parsedValue, {
957
+ cwd: repoRoot,
958
+ env: dependencies.processEnv,
959
+ });
960
+ const warning = dispatchProviderAvailabilityWarning(key, parsedValue, availability);
961
+ if (warning) {
962
+ warn(warning);
963
+ }
964
+ }
778
965
  if (effectiveSurface === 'user') {
779
966
  const userConfig = await dependencies.readUserConfig(userConfigDir);
780
967
  await dependencies.writeUserConfig(userConfigDir, {
@@ -952,6 +1139,109 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
952
1139
  source: 'shared',
953
1140
  };
954
1141
  }
1142
+ function hasExistingDispatchMatrix(config) {
1143
+ const providers = config.workflow?.dispatchCeiling?.providers;
1144
+ return providers !== undefined && Object.keys(providers).length > 0;
1145
+ }
1146
+ async function loadDispatchMatrixRecommendation(dependencies) {
1147
+ const assetsRoot = await dependencies.resolveAssetsRoot();
1148
+ const assetPath = join(assetsRoot, DISPATCH_MATRIX_RECOMMENDATION_ASSET);
1149
+ return parseDispatchMatrixRecommendation(await dependencies.readFile(assetPath));
1150
+ }
1151
+ async function validateRecommendationCells(repoRoot, recommendation, dependencies, warn) {
1152
+ for (const ref of collectDispatchMatrixCellRefs(recommendation.providers)) {
1153
+ const closedValues = closedDispatchProviderValues(ref.provider);
1154
+ if (closedValues && !closedValues.includes(ref.value)) {
1155
+ throw new Error(`Invalid value for ${ref.path}: expected one of ${closedValues.join(' | ')}, got '${ref.value}'`);
1156
+ }
1157
+ let availability;
1158
+ try {
1159
+ availability = await dependencies.validateMatrixCell(ref.provider, ref.value, {
1160
+ cwd: repoRoot,
1161
+ env: dependencies.processEnv,
1162
+ });
1163
+ }
1164
+ catch {
1165
+ availability = 'unvalidated';
1166
+ }
1167
+ const warning = matrixCellAvailabilityWarning(ref.path, ref.value, availability);
1168
+ if (warning) {
1169
+ warn(warning);
1170
+ }
1171
+ }
1172
+ }
1173
+ function applyDispatchMatrixRecommendation(workflow, recommendation) {
1174
+ return {
1175
+ ...(workflow ?? {}),
1176
+ dispatchCeiling: {
1177
+ ...workflow?.dispatchCeiling,
1178
+ recommendationVersion: recommendation.version,
1179
+ providers: recommendation.providers,
1180
+ },
1181
+ };
1182
+ }
1183
+ async function confirmDispatchMatrixOverwrite(context, dependencies, source, yes) {
1184
+ if (yes) {
1185
+ return;
1186
+ }
1187
+ if (!context.interactive) {
1188
+ throw new Error('Dispatch matrix already exists; rerun interactively or pass --yes to replace it.');
1189
+ }
1190
+ const shouldReplace = await dependencies.confirmAction(`Replace existing dispatch matrix in ${source} config?`, { interactive: context.interactive });
1191
+ if (!shouldReplace) {
1192
+ throw new Error('Dispatch matrix adoption cancelled; existing matrix unchanged.');
1193
+ }
1194
+ }
1195
+ async function adoptDispatchMatrixRecommendation(repoRoot, userConfigDir, options, context, dependencies) {
1196
+ const source = options.surface === 'auto' ? 'local' : options.surface;
1197
+ const recommendation = await loadDispatchMatrixRecommendation(dependencies);
1198
+ if (source === 'user') {
1199
+ const userConfig = await dependencies.readUserConfig(userConfigDir);
1200
+ if (hasExistingDispatchMatrix(userConfig)) {
1201
+ await confirmDispatchMatrixOverwrite(context, dependencies, source, options.yes);
1202
+ }
1203
+ await validateRecommendationCells(repoRoot, recommendation, dependencies, context.logger.warn);
1204
+ await dependencies.writeUserConfig(userConfigDir, {
1205
+ ...userConfig,
1206
+ workflow: applyDispatchMatrixRecommendation(userConfig.workflow, recommendation),
1207
+ });
1208
+ return {
1209
+ key: 'workflow.dispatchCeiling.providers',
1210
+ value: recommendation.version,
1211
+ source,
1212
+ };
1213
+ }
1214
+ if (source === 'local') {
1215
+ const localConfig = await dependencies.readOatLocalConfig(repoRoot);
1216
+ if (hasExistingDispatchMatrix(localConfig)) {
1217
+ await confirmDispatchMatrixOverwrite(context, dependencies, source, options.yes);
1218
+ }
1219
+ await validateRecommendationCells(repoRoot, recommendation, dependencies, context.logger.warn);
1220
+ await dependencies.writeOatLocalConfig(repoRoot, {
1221
+ ...localConfig,
1222
+ workflow: applyDispatchMatrixRecommendation(localConfig.workflow, recommendation),
1223
+ });
1224
+ return {
1225
+ key: 'workflow.dispatchCeiling.providers',
1226
+ value: recommendation.version,
1227
+ source,
1228
+ };
1229
+ }
1230
+ const sharedConfig = await dependencies.readOatConfig(repoRoot);
1231
+ if (hasExistingDispatchMatrix(sharedConfig)) {
1232
+ await confirmDispatchMatrixOverwrite(context, dependencies, source, options.yes);
1233
+ }
1234
+ await validateRecommendationCells(repoRoot, recommendation, dependencies, context.logger.warn);
1235
+ await dependencies.writeOatConfig(repoRoot, {
1236
+ ...sharedConfig,
1237
+ workflow: applyDispatchMatrixRecommendation(sharedConfig.workflow, recommendation),
1238
+ });
1239
+ return {
1240
+ key: 'workflow.dispatchCeiling.providers',
1241
+ value: recommendation.version,
1242
+ source,
1243
+ };
1244
+ }
955
1245
  function formatList(values) {
956
1246
  const keyWidth = Math.max('Key'.length, ...values.map((item) => item.key.length));
957
1247
  const sourceWidth = Math.max('Source'.length, ...values.map((item) => item.source.length));
@@ -1044,7 +1334,7 @@ async function runSet(keyArg, rawValue, surface, context, dependencies) {
1044
1334
  }
1045
1335
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
1046
1336
  const userConfigDir = join(context.home, '.oat');
1047
- const result = await setConfigValue(repoRoot, userConfigDir, keyArg, rawValue, surface, dependencies);
1337
+ const result = await setConfigValue(repoRoot, userConfigDir, keyArg, rawValue, surface, dependencies, context.logger.warn);
1048
1338
  if (context.json) {
1049
1339
  context.logger.json({
1050
1340
  status: 'ok',
@@ -1067,12 +1357,42 @@ async function runSet(keyArg, rawValue, surface, context, dependencies) {
1067
1357
  process.exitCode = 1;
1068
1358
  }
1069
1359
  }
1360
+ async function runAdopt(templateArg, options, context, dependencies) {
1361
+ try {
1362
+ if (templateArg !== 'dispatch-matrix') {
1363
+ throw new Error(`Unknown config adoption template: ${templateArg}`);
1364
+ }
1365
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
1366
+ const userConfigDir = join(context.home, '.oat');
1367
+ const result = await adoptDispatchMatrixRecommendation(repoRoot, userConfigDir, options, context, dependencies);
1368
+ if (context.json) {
1369
+ context.logger.json({
1370
+ status: 'ok',
1371
+ ...result,
1372
+ });
1373
+ }
1374
+ else {
1375
+ context.logger.info(`Adopted dispatch matrix recommendation ${result.value} to ${result.source} config.`);
1376
+ }
1377
+ process.exitCode = 0;
1378
+ }
1379
+ catch (error) {
1380
+ const message = error instanceof Error ? error.message : String(error);
1381
+ if (context.json) {
1382
+ context.logger.json({ status: 'error', message });
1383
+ }
1384
+ else {
1385
+ context.logger.error(message);
1386
+ }
1387
+ process.exitCode = 1;
1388
+ }
1389
+ }
1070
1390
  async function runList(context, dependencies) {
1071
1391
  try {
1072
1392
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
1073
1393
  const userConfigDir = join(context.home, '.oat');
1074
1394
  const values = [];
1075
- for (const key of KEY_ORDER) {
1395
+ for (const key of await listConfigKeys(repoRoot, userConfigDir, dependencies)) {
1076
1396
  values.push(await getConfigValue(repoRoot, userConfigDir, key, dependencies));
1077
1397
  }
1078
1398
  if (context.json) {
@@ -1182,6 +1502,44 @@ export function createConfigCommand(overrides = {}) {
1182
1502
  }
1183
1503
  process.exitCode = 1;
1184
1504
  }
1505
+ }))
1506
+ .addCommand(new Command('adopt')
1507
+ .description('Adopt a bundled OAT config recommendation')
1508
+ .argument('<template>', 'Recommendation template to adopt')
1509
+ .option('--shared', 'Write to the shared repo config (.oat/config.json)')
1510
+ .option('--local', 'Write to the repo-local config (.oat/config.local.json)')
1511
+ .option('--user', 'Write to the user-level config (~/.oat/config.json)')
1512
+ .option('--yes', 'Replace an existing adopted matrix without prompting')
1513
+ .action(async (template, options, command) => {
1514
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
1515
+ try {
1516
+ const flagsPresent = [
1517
+ options.shared,
1518
+ options.local,
1519
+ options.user,
1520
+ ].filter(Boolean).length;
1521
+ if (flagsPresent > 1) {
1522
+ throw new Error('--shared, --local, and --user flags are mutually exclusive; pass at most one.');
1523
+ }
1524
+ let surface = 'auto';
1525
+ if (options.shared)
1526
+ surface = 'shared';
1527
+ else if (options.local)
1528
+ surface = 'local';
1529
+ else if (options.user)
1530
+ surface = 'user';
1531
+ await runAdopt(template, { surface, yes: options.yes === true }, context, dependencies);
1532
+ }
1533
+ catch (error) {
1534
+ const message = error instanceof Error ? error.message : String(error);
1535
+ if (context.json) {
1536
+ context.logger.json({ status: 'error', message });
1537
+ }
1538
+ else {
1539
+ context.logger.error(message);
1540
+ }
1541
+ process.exitCode = 1;
1542
+ }
1185
1543
  }))
1186
1544
  .addCommand(new Command('list')
1187
1545
  .description('List resolved OAT config values with sources')
@@ -1,7 +1,8 @@
1
1
  import { type CommandContext, type GlobalOptions } from '../../app/command-context.js';
2
2
  import { type PjmDoctorOptions } from '../pjm/doctor.js';
3
- import { type OatConfig } from '../../config/oat-config.js';
3
+ import { type OatConfig, type OatLocalConfig, type UserConfig } from '../../config/oat-config.js';
4
4
  import { type Manifest } from '../../manifest/index.js';
5
+ import { type MatrixCellAvailability, type ValidateMatrixCellOptions } from '../../providers/identity/availability.js';
5
6
  import type { ConcreteScope } from '../../shared/types.js';
6
7
  import { type DoctorCheck } from '../../ui/output.js';
7
8
  import { Command } from 'commander';
@@ -19,6 +20,10 @@ interface DoctorDependencies {
19
20
  readFile: (path: string) => Promise<string>;
20
21
  resolveAssetsRoot: () => Promise<string>;
21
22
  readOatConfig: (repoRoot: string) => Promise<OatConfig>;
23
+ readOatLocalConfig: (repoRoot: string) => Promise<OatLocalConfig>;
24
+ readUserConfig: (userConfigDir: string) => Promise<UserConfig>;
25
+ validateMatrixCell: (provider: string, value: string, options: ValidateMatrixCellOptions) => Promise<MatrixCellAvailability>;
26
+ processEnv: NodeJS.ProcessEnv;
22
27
  runPjmDoctorChecks: (repoRoot: string, options?: PjmDoctorOptions) => Promise<DoctorCheck[]>;
23
28
  checkSkillVersions: (scopeRoot: string, assetsRoot: string, pathExists: (path: string) => Promise<boolean>) => Promise<SkillVersionReport>;
24
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAInE,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAM9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,KAAK,WAAW,EAAuB,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,UAAU,kBAAkB;IAC1B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,gBAAgB,EAAE,CAChB,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,cAAc,KACpB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,cAAc,EAAE,CACd,SAAS,EAAE,MAAM,KACd,OAAO,CACV,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CACnE,CAAC;IACF,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,iBAAiB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,kBAAkB,EAAE,CAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,gBAAgB,KACvB,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5B,kBAAkB,EAAE,CAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,KAC3C,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAClC;AAED,UAAU,oBAAoB;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,UAAU,kBAAkB;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,0BAA0B,EAAE,MAAM,CAAC;IACnC,cAAc,EAAE,oBAAoB,EAAE,CAAC;CACxC;AAsbD,wBAAgB,mBAAmB,CACjC,SAAS,GAAE,OAAO,CAAC,kBAAkB,CAAM,GAC1C,OAAO,CAcT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/index.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@ import { createPjmDisabledCheck, runPjmDoctorChecks, } from '../pjm/doctor.js';
7
7
  import { getSkillVersion } from '../shared/frontmatter.js';
8
8
  import { withScopeOption } from '../shared/scope-option.js';
9
9
  import { readGlobalOptions, resolveConcreteScopes, } from '../shared/shared.utils.js';
10
- import { readOatConfig } from '../../config/oat-config.js';
10
+ import { readOatConfig, readOatLocalConfig, readUserConfig, } from '../../config/oat-config.js';
11
11
  import { resolveAssetsRoot } from '../../fs/assets.js';
12
12
  import { resolveProjectRoot, resolveScopeRoot } from '../../fs/paths.js';
13
13
  import TOML from '@iarna/toml';
@@ -17,6 +17,7 @@ import { codexAdapter } from '../../providers/codex/index.js';
17
17
  import { copilotAdapter } from '../../providers/copilot/index.js';
18
18
  import { cursorAdapter } from '../../providers/cursor/index.js';
19
19
  import { geminiAdapter } from '../../providers/gemini/index.js';
20
+ import { validateMatrixCell, } from '../../providers/identity/availability.js';
20
21
  import { formatDoctorResults } from '../../ui/output.js';
21
22
  import { Command } from 'commander';
22
23
  async function pathExistsDefault(path) {
@@ -129,13 +130,137 @@ function createDependencies() {
129
130
  readFile: async (path) => readFile(path, 'utf8'),
130
131
  resolveAssetsRoot,
131
132
  readOatConfig,
133
+ readOatLocalConfig,
134
+ readUserConfig,
135
+ validateMatrixCell,
136
+ processEnv: process.env,
132
137
  runPjmDoctorChecks,
133
138
  // Default binding remains self-contained, but still honors the caller-
134
139
  // provided pathExists dependency from runChecksForScope when available.
135
140
  checkSkillVersions: (scopeRoot, assetsRoot, pathExists = pathExistsDefault) => checkSkillVersionsDefault(scopeRoot, assetsRoot, pathExists),
136
141
  };
137
142
  }
138
- async function runChecksForScope(scope, scopeRoot, dependencies) {
143
+ function isRouteTarget(entry) {
144
+ return typeof entry === 'object' && entry !== null && !Array.isArray(entry);
145
+ }
146
+ function addDispatchMatrixCellRefs(refs, layer, provider, path, cell) {
147
+ if (typeof cell === 'string') {
148
+ refs.push({ layer, provider, value: cell, path });
149
+ return;
150
+ }
151
+ for (const [index, entry] of cell.entries()) {
152
+ const entryPath = `${path}[${index}]`;
153
+ if (typeof entry === 'string') {
154
+ refs.push({ layer, provider, value: entry, path: entryPath });
155
+ continue;
156
+ }
157
+ if (!isRouteTarget(entry)) {
158
+ continue;
159
+ }
160
+ if (entry.model) {
161
+ refs.push({
162
+ layer,
163
+ provider,
164
+ value: entry.model,
165
+ path: `${entryPath}.model`,
166
+ });
167
+ }
168
+ if (entry.effort) {
169
+ refs.push({
170
+ layer,
171
+ provider,
172
+ value: entry.effort,
173
+ path: `${entryPath}.effort`,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ function collectDispatchMatrixCellRefs(layers) {
179
+ const refs = [];
180
+ for (const { layer, config } of layers) {
181
+ const providers = config.workflow?.dispatchCeiling?.providers ?? {};
182
+ for (const [provider, providerValue] of Object.entries(providers)) {
183
+ const providerPath = `workflow.dispatchCeiling.providers.${provider}`;
184
+ if (typeof providerValue === 'string') {
185
+ refs.push({
186
+ layer,
187
+ provider,
188
+ value: providerValue,
189
+ path: providerPath,
190
+ });
191
+ continue;
192
+ }
193
+ const tierMap = providerValue;
194
+ for (const [tier, cell] of Object.entries(tierMap)) {
195
+ if (cell === undefined) {
196
+ continue;
197
+ }
198
+ addDispatchMatrixCellRefs(refs, layer, provider, `${providerPath}.${tier}`, cell);
199
+ }
200
+ }
201
+ }
202
+ return refs;
203
+ }
204
+ function formatDispatchMatrixIssueList(issues) {
205
+ return issues
206
+ .map((issue) => `${issue.path}=${issue.value} (${issue.layer} config)`)
207
+ .join(', ');
208
+ }
209
+ async function createDispatchMatrixDoctorCheck(scopeRoot, layers, dependencies) {
210
+ const refs = collectDispatchMatrixCellRefs(layers);
211
+ if (refs.length === 0) {
212
+ return {
213
+ name: 'project:dispatch_matrix',
214
+ description: 'Dispatch matrix cell availability',
215
+ status: 'pass',
216
+ message: 'No configured dispatch matrix cells found in user, shared, or local config layers.',
217
+ };
218
+ }
219
+ const issues = [];
220
+ for (const ref of refs) {
221
+ let availability;
222
+ try {
223
+ availability = await dependencies.validateMatrixCell(ref.provider, ref.value, {
224
+ cwd: scopeRoot,
225
+ env: dependencies.processEnv,
226
+ });
227
+ }
228
+ catch {
229
+ availability = 'unvalidated';
230
+ }
231
+ if (availability !== 'valid') {
232
+ issues.push({ ...ref, availability });
233
+ }
234
+ }
235
+ if (issues.length === 0) {
236
+ return {
237
+ name: 'project:dispatch_matrix',
238
+ description: 'Dispatch matrix cell availability',
239
+ status: 'pass',
240
+ message: `All configured dispatch matrix cells are available: ${formatDispatchMatrixIssueList(refs)}.`,
241
+ };
242
+ }
243
+ const unknown = issues.filter((issue) => issue.availability === 'unknown-value');
244
+ const unvalidated = issues.filter((issue) => issue.availability === 'unvalidated');
245
+ const messageParts = [];
246
+ if (unknown.length > 0) {
247
+ messageParts.push(`Unknown dispatch matrix cells: ${formatDispatchMatrixIssueList(unknown)}`);
248
+ }
249
+ if (unvalidated.length > 0) {
250
+ messageParts.push(`Unvalidated dispatch matrix cells: ${formatDispatchMatrixIssueList(unvalidated)}`);
251
+ }
252
+ const hasUnknown = unknown.length > 0;
253
+ return {
254
+ name: 'project:dispatch_matrix',
255
+ description: 'Dispatch matrix cell availability',
256
+ status: hasUnknown ? 'warn' : 'pass',
257
+ message: messageParts.join('. '),
258
+ fix: hasUnknown
259
+ ? 'Run `oat config set workflow.dispatchCeiling.providers.<provider> <value>` with an available value, or refresh provider assets with `oat sync --scope project`.'
260
+ : undefined,
261
+ };
262
+ }
263
+ async function runChecksForScope(scope, scopeRoot, userConfigDir, dependencies) {
139
264
  const checks = [];
140
265
  const skillsPath = join(scopeRoot, '.agents', 'skills');
141
266
  const agentsPath = join(scopeRoot, '.agents', 'agents');
@@ -345,7 +470,16 @@ async function runChecksForScope(scope, scopeRoot, dependencies) {
345
470
  }
346
471
  }
347
472
  const repoReferenceRoot = join(scopeRoot, '.oat', 'repo');
348
- const config = await dependencies.readOatConfig(scopeRoot);
473
+ const [userConfig, config, localConfig] = await Promise.all([
474
+ dependencies.readUserConfig(userConfigDir),
475
+ dependencies.readOatConfig(scopeRoot),
476
+ dependencies.readOatLocalConfig(scopeRoot),
477
+ ]);
478
+ checks.push(await createDispatchMatrixDoctorCheck(scopeRoot, [
479
+ { layer: 'user', config: userConfig },
480
+ { layer: 'shared', config },
481
+ { layer: 'local', config: localConfig },
482
+ ], dependencies));
349
483
  const projectManagementEnabled = config.tools?.['project-management'] === true;
350
484
  if (projectManagementEnabled) {
351
485
  checks.push(...(await dependencies.runPjmDoctorChecks(repoReferenceRoot, {
@@ -362,7 +496,7 @@ async function runDoctorCommand(context, dependencies) {
362
496
  const checks = [];
363
497
  for (const scope of resolveConcreteScopes(context.scope)) {
364
498
  const scopeRoot = await dependencies.resolveScopeRoot(scope, context);
365
- const scopeChecks = await runChecksForScope(scope, scopeRoot, dependencies);
499
+ const scopeChecks = await runChecksForScope(scope, scopeRoot, join(context.home, '.oat'), dependencies);
366
500
  checks.push(...scopeChecks);
367
501
  }
368
502
  if (context.json) {