@open-agent-toolkit/cli 0.1.42 → 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 (60) hide show
  1. package/assets/agents/oat-phase-implementer.md +11 -7
  2. package/assets/agents/oat-reviewer.md +6 -4
  3. package/assets/config/dispatch-matrix-recommendation.json +23 -0
  4. package/assets/docs/cli-utilities/configuration.md +95 -40
  5. package/assets/docs/cli-utilities/workflow-gates.md +79 -16
  6. package/assets/docs/contributing/code.md +10 -4
  7. package/assets/docs/contributing/hooks-and-safety.md +23 -0
  8. package/assets/docs/reference/oat-directory-structure.md +26 -24
  9. package/assets/docs/workflows/projects/dispatch-ceiling.md +193 -80
  10. package/assets/docs/workflows/projects/implementation-execution.md +68 -45
  11. package/assets/docs/workflows/projects/index.md +2 -2
  12. package/assets/docs/workflows/projects/lifecycle.md +17 -2
  13. package/assets/public-package-versions.json +4 -4
  14. package/assets/skills/oat-project-implement/SKILL.md +232 -100
  15. package/assets/skills/oat-project-plan/SKILL.md +80 -30
  16. package/assets/skills/oat-project-plan-writing/SKILL.md +10 -10
  17. package/assets/skills/oat-project-quick-start/SKILL.md +80 -30
  18. package/assets/templates/plan.md +4 -4
  19. package/assets/templates/state.md +12 -3
  20. package/dist/commands/config/index.d.ts +6 -0
  21. package/dist/commands/config/index.d.ts.map +1 -1
  22. package/dist/commands/config/index.js +432 -11
  23. package/dist/commands/doctor/index.d.ts +6 -1
  24. package/dist/commands/doctor/index.d.ts.map +1 -1
  25. package/dist/commands/doctor/index.js +138 -4
  26. package/dist/commands/gate/index.d.ts +39 -2
  27. package/dist/commands/gate/index.d.ts.map +1 -1
  28. package/dist/commands/gate/index.js +343 -30
  29. package/dist/commands/internal/cursor-current-target.d.ts +37 -0
  30. package/dist/commands/internal/cursor-current-target.d.ts.map +1 -0
  31. package/dist/commands/internal/cursor-current-target.js +228 -0
  32. package/dist/commands/internal/index.d.ts.map +1 -1
  33. package/dist/commands/internal/index.js +2 -0
  34. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  35. package/dist/commands/project/dispatch-ceiling/index.js +646 -56
  36. package/dist/config/dispatch-ceiling-preset.d.ts +37 -1
  37. package/dist/config/dispatch-ceiling-preset.d.ts.map +1 -1
  38. package/dist/config/dispatch-ceiling-preset.js +20 -0
  39. package/dist/config/oat-config.d.ts +25 -6
  40. package/dist/config/oat-config.d.ts.map +1 -1
  41. package/dist/config/oat-config.js +123 -11
  42. package/dist/config/resolve.d.ts.map +1 -1
  43. package/dist/config/resolve.js +16 -0
  44. package/dist/manifest/manifest.types.d.ts +12 -12
  45. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  46. package/dist/providers/ceiling/registry.js +22 -1
  47. package/dist/providers/codex/codec/sync-extension.js +1 -1
  48. package/dist/providers/identity/availability.d.ts +22 -0
  49. package/dist/providers/identity/availability.d.ts.map +1 -0
  50. package/dist/providers/identity/availability.js +119 -0
  51. package/dist/providers/identity/family.d.ts +13 -0
  52. package/dist/providers/identity/family.d.ts.map +1 -0
  53. package/dist/providers/identity/family.js +32 -0
  54. package/dist/providers/identity/provenance.d.ts +21 -0
  55. package/dist/providers/identity/provenance.d.ts.map +1 -0
  56. package/dist/providers/identity/provenance.js +65 -0
  57. package/dist/providers/identity/stamp.d.ts +35 -0
  58. package/dist/providers/identity/stamp.d.ts.map +1 -0
  59. package/dist/providers/identity/stamp.js +171 -0
  60. 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',
@@ -43,6 +54,8 @@ const KEY_ORDER = [
43
54
  'workflow.autoArtifactReview.plan',
44
55
  'workflow.autoArtifactReview.analysis',
45
56
  'workflow.designMode',
57
+ 'workflow.dispatchPolicy.mode',
58
+ 'workflow.dispatchPolicy.policy',
46
59
  'workflow.dispatchCeiling.preset',
47
60
  'workflow.dispatchCeiling.providers.codex',
48
61
  'workflow.dispatchCeiling.providers.claude',
@@ -454,7 +467,29 @@ const CONFIG_CATALOG = [
454
467
  defaultValue: 'unset',
455
468
  mutability: 'read/write',
456
469
  owningCommand: 'oat config set workflow.dispatchCeiling.preset <value>',
457
- description: 'Provider-neutral ceiling preset that compiles to concrete per-provider values at write time. balanced → Codex: high, Claude: sonnet; maximum → Codex: xhigh, Claude: opus; cost-conscious → Codex: medium, Claude: sonnet. Preset provenance only; runtime dispatch reads concrete providers values. Resolution: local > shared > user > default.',
470
+ description: 'Legacy compatibility alias for capped managed dispatch policies. Provider-neutral ceiling preset that compiles to concrete per-provider values at write time. balanced → Codex: high, Claude: sonnet; maximum → Codex: xhigh, Claude: opus; cost-conscious → Codex: medium, Claude: sonnet. Preset provenance only; runtime dispatch reads concrete providers values. Resolution: local > shared > user > default.',
471
+ },
472
+ {
473
+ key: 'workflow.dispatchPolicy.mode',
474
+ group: 'Workflow Preferences (3-layer: local > shared > user)',
475
+ file: '.oat/config.local.json | .oat/config.json | ~/.oat/config.json',
476
+ scope: 'workflow',
477
+ type: 'managed | inherit',
478
+ defaultValue: 'unset',
479
+ mutability: 'read/write',
480
+ owningCommand: 'oat config set workflow.dispatchPolicy.mode <value>',
481
+ description: 'Dispatch policy mode. "managed" means OAT selects model/effort from workflow.dispatchPolicy.policy; "inherit" means OAT leaves dispatch controls to the host/provider defaults. Set workflow.dispatchPolicy.policy to choose a managed policy. Resolution: local > shared > user > default.',
482
+ },
483
+ {
484
+ key: 'workflow.dispatchPolicy.policy',
485
+ group: 'Workflow Preferences (3-layer: local > shared > user)',
486
+ file: '.oat/config.local.json | .oat/config.json | ~/.oat/config.json',
487
+ scope: 'workflow',
488
+ type: 'economy | balanced | high | frontier | uncapped',
489
+ defaultValue: 'unset',
490
+ mutability: 'read/write',
491
+ owningCommand: 'oat config set workflow.dispatchPolicy.policy <value>',
492
+ description: 'Managed dispatch policy. economy, balanced, high, and frontier are capped managed policies; uncapped keeps OAT-managed preferred selection without provider caps. Setting this key writes workflow.dispatchPolicy.mode=managed. Resolution: local > shared > user > default.',
458
493
  },
459
494
  {
460
495
  key: 'workflow.dispatchCeiling.providers.codex',
@@ -472,7 +507,7 @@ const CONFIG_CATALOG = [
472
507
  group: 'Workflow Preferences (3-layer: local > shared > user)',
473
508
  file: '.oat/config.local.json | .oat/config.json | ~/.oat/config.json',
474
509
  scope: 'workflow',
475
- type: 'haiku | sonnet | opus',
510
+ type: 'haiku | sonnet | opus | fable',
476
511
  defaultValue: 'unset',
477
512
  mutability: 'read/write',
478
513
  owningCommand: 'oat config set workflow.dispatchCeiling.providers.claude <value>',
@@ -523,10 +558,49 @@ const DEFAULT_DEPENDENCIES = {
523
558
  writeUserConfig,
524
559
  resolveProjectsRoot,
525
560
  resolveEffectiveConfig,
561
+ resolveAssetsRoot,
562
+ readFile: (path) => readFileDefault(path, 'utf8'),
563
+ confirmAction,
564
+ validateMatrixCell,
526
565
  processEnv: process.env,
527
566
  };
528
567
  function isConfigKey(value) {
529
- 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;
530
604
  }
531
605
  function normalizeSharedRoot(value) {
532
606
  const trimmed = value.trim();
@@ -540,6 +614,14 @@ const WORKFLOW_ENUM_VALUES = {
540
614
  'workflow.postImplementSequence': ['wait', 'summary', 'pr', 'docs-pr'],
541
615
  'workflow.reviewExecutionModel': ['subagent', 'inline', 'fresh-session'],
542
616
  'workflow.designMode': ['collaborative', 'selective', 'draft'],
617
+ 'workflow.dispatchPolicy.mode': ['managed', 'inherit'],
618
+ 'workflow.dispatchPolicy.policy': [
619
+ 'economy',
620
+ 'balanced',
621
+ 'high',
622
+ 'frontier',
623
+ 'uncapped',
624
+ ],
543
625
  'workflow.dispatchCeiling.preset': ['balanced', 'maximum', 'cost-conscious'],
544
626
  'workflow.dispatchCeiling.providers.codex': [
545
627
  'low',
@@ -547,8 +629,22 @@ const WORKFLOW_ENUM_VALUES = {
547
629
  'high',
548
630
  'xhigh',
549
631
  ],
550
- 'workflow.dispatchCeiling.providers.claude': ['haiku', 'sonnet', 'opus'],
632
+ 'workflow.dispatchCeiling.providers.claude': [
633
+ 'haiku',
634
+ 'sonnet',
635
+ 'opus',
636
+ 'fable',
637
+ ],
551
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
+ }
552
648
  const WORKFLOW_BOOLEAN_KEYS = new Set([
553
649
  'workflow.archiveOnComplete',
554
650
  'workflow.createPrOnComplete',
@@ -629,10 +725,123 @@ function parseWorkflowValue(key, rawValue) {
629
725
  }
630
726
  return normalized;
631
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
+ }
632
740
  throw new Error(`Unknown workflow key: ${key}`);
633
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
+ }
634
817
  function applyWorkflowValue(workflow, key, value) {
635
818
  const subKey = key.slice('workflow.'.length);
819
+ if (subKey === 'dispatchPolicy.mode') {
820
+ const mode = value;
821
+ if (mode === 'inherit') {
822
+ return {
823
+ ...workflow,
824
+ dispatchPolicy: { mode },
825
+ };
826
+ }
827
+ const policy = workflow.dispatchPolicy?.policy;
828
+ if (!policy) {
829
+ throw new Error('Cannot set workflow.dispatchPolicy.mode to managed without an existing workflow.dispatchPolicy.policy. Set workflow.dispatchPolicy.policy <economy|balanced|high|frontier|uncapped> instead.');
830
+ }
831
+ return {
832
+ ...workflow,
833
+ dispatchPolicy: { mode, policy },
834
+ };
835
+ }
836
+ if (subKey === 'dispatchPolicy.policy') {
837
+ return {
838
+ ...workflow,
839
+ dispatchPolicy: {
840
+ mode: 'managed',
841
+ policy: value,
842
+ },
843
+ };
844
+ }
636
845
  if (subKey === 'dispatchCeiling.preset') {
637
846
  // Presets compile to concrete per-provider values at write time so the
638
847
  // resolver (which reads only providers.*) gets a usable ceiling. The preset
@@ -651,15 +860,37 @@ function applyWorkflowValue(workflow, key, value) {
651
860
  },
652
861
  };
653
862
  }
654
- if (subKey.startsWith('dispatchCeiling.providers.')) {
655
- 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
+ }
656
887
  return {
657
888
  ...workflow,
658
889
  dispatchCeiling: {
659
890
  ...workflow.dispatchCeiling,
660
891
  providers: {
661
- ...workflow.dispatchCeiling?.providers,
662
- [providerKey]: value,
892
+ ...providers,
893
+ [provider]: value,
663
894
  },
664
895
  },
665
896
  };
@@ -706,12 +937,31 @@ async function getConfigValue(repoRoot, userConfigDir, key, dependencies) {
706
937
  source: entry.source,
707
938
  };
708
939
  }
709
- 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) {
710
950
  validateSurfaceForKey(key, surface);
711
951
  const effectiveSurface = surface === 'auto' ? defaultSurfaceForKey(key) : surface;
712
952
  if (isWorkflowKey(key)) {
713
953
  const parsedValue = parseWorkflowValue(key, rawValue);
714
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
+ }
715
965
  if (effectiveSurface === 'user') {
716
966
  const userConfig = await dependencies.readUserConfig(userConfigDir);
717
967
  await dependencies.writeUserConfig(userConfigDir, {
@@ -889,6 +1139,109 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
889
1139
  source: 'shared',
890
1140
  };
891
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
+ }
892
1245
  function formatList(values) {
893
1246
  const keyWidth = Math.max('Key'.length, ...values.map((item) => item.key.length));
894
1247
  const sourceWidth = Math.max('Source'.length, ...values.map((item) => item.source.length));
@@ -981,7 +1334,7 @@ async function runSet(keyArg, rawValue, surface, context, dependencies) {
981
1334
  }
982
1335
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
983
1336
  const userConfigDir = join(context.home, '.oat');
984
- const result = await setConfigValue(repoRoot, userConfigDir, keyArg, rawValue, surface, dependencies);
1337
+ const result = await setConfigValue(repoRoot, userConfigDir, keyArg, rawValue, surface, dependencies, context.logger.warn);
985
1338
  if (context.json) {
986
1339
  context.logger.json({
987
1340
  status: 'ok',
@@ -1004,12 +1357,42 @@ async function runSet(keyArg, rawValue, surface, context, dependencies) {
1004
1357
  process.exitCode = 1;
1005
1358
  }
1006
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
+ }
1007
1390
  async function runList(context, dependencies) {
1008
1391
  try {
1009
1392
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
1010
1393
  const userConfigDir = join(context.home, '.oat');
1011
1394
  const values = [];
1012
- for (const key of KEY_ORDER) {
1395
+ for (const key of await listConfigKeys(repoRoot, userConfigDir, dependencies)) {
1013
1396
  values.push(await getConfigValue(repoRoot, userConfigDir, key, dependencies));
1014
1397
  }
1015
1398
  if (context.json) {
@@ -1119,6 +1502,44 @@ export function createConfigCommand(overrides = {}) {
1119
1502
  }
1120
1503
  process.exitCode = 1;
1121
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
+ }
1122
1543
  }))
1123
1544
  .addCommand(new Command('list')
1124
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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"}