@open-agent-toolkit/cli 0.1.46 → 0.1.48

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 (82) hide show
  1. package/assets/agents/oat-phase-implementer.md +202 -191
  2. package/assets/agents/oat-reviewer.md +11 -1
  3. package/assets/config/dispatch-matrix-recommendation.json +120 -13
  4. package/assets/docs/cli-utilities/configuration.md +172 -113
  5. package/assets/docs/cli-utilities/workflow-gates.md +161 -27
  6. package/assets/docs/contributing/skills.md +14 -8
  7. package/assets/docs/provider-sync/providers.md +41 -7
  8. package/assets/docs/provider-sync/scope-and-surface.md +2 -2
  9. package/assets/docs/reference/cli-reference.md +3 -1
  10. package/assets/docs/reference/oat-directory-structure.md +27 -26
  11. package/assets/docs/workflows/projects/artifacts.md +31 -1
  12. package/assets/docs/workflows/projects/dispatch-ceiling.md +235 -187
  13. package/assets/docs/workflows/projects/implementation-execution.md +283 -260
  14. package/assets/docs/workflows/projects/lifecycle.md +26 -5
  15. package/assets/docs/workflows/projects/reviews.md +27 -2
  16. package/assets/migration/pjm-restructure.md +1 -1
  17. package/assets/public-package-versions.json +4 -4
  18. package/assets/skills/oat-project-implement/SKILL.md +247 -133
  19. package/assets/skills/oat-project-import-plan/SKILL.md +173 -16
  20. package/assets/skills/oat-project-next/SKILL.md +2 -2
  21. package/assets/skills/oat-project-plan/SKILL.md +122 -92
  22. package/assets/skills/oat-project-plan-writing/SKILL.md +246 -15
  23. package/assets/skills/oat-project-quick-start/SKILL.md +157 -107
  24. package/assets/skills/oat-project-review-provide/SKILL.md +94 -22
  25. package/dist/commands/config/index.d.ts.map +1 -1
  26. package/dist/commands/config/index.js +64 -39
  27. package/dist/commands/doctor/index.d.ts.map +1 -1
  28. package/dist/commands/doctor/index.js +21 -6
  29. package/dist/commands/gate/index.d.ts +7 -1
  30. package/dist/commands/gate/index.d.ts.map +1 -1
  31. package/dist/commands/gate/index.js +544 -67
  32. package/dist/commands/gate/review-verdict.d.ts +16 -0
  33. package/dist/commands/gate/review-verdict.d.ts.map +1 -1
  34. package/dist/commands/gate/review-verdict.js +72 -9
  35. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  36. package/dist/commands/project/dispatch-ceiling/index.js +452 -16
  37. package/dist/commands/providers/codex/materialize.d.ts.map +1 -1
  38. package/dist/commands/providers/codex/materialize.js +6 -1
  39. package/dist/commands/shared/frontmatter.d.ts +4 -0
  40. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  41. package/dist/commands/shared/frontmatter.js +23 -0
  42. package/dist/commands/status/index.d.ts +1 -0
  43. package/dist/commands/status/index.d.ts.map +1 -1
  44. package/dist/commands/status/index.js +10 -4
  45. package/dist/commands/sync/index.d.ts.map +1 -1
  46. package/dist/commands/sync/index.js +10 -3
  47. package/dist/commands/sync/sync.types.d.ts +1 -0
  48. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  49. package/dist/config/oat-config.d.ts +19 -2
  50. package/dist/config/oat-config.d.ts.map +1 -1
  51. package/dist/config/oat-config.js +139 -24
  52. package/dist/config/resolve.d.ts +8 -0
  53. package/dist/config/resolve.d.ts.map +1 -1
  54. package/dist/config/resolve.js +66 -2
  55. package/dist/engine/index.d.ts +1 -1
  56. package/dist/engine/index.d.ts.map +1 -1
  57. package/dist/engine/index.js +1 -1
  58. package/dist/engine/scanner.d.ts +1 -0
  59. package/dist/engine/scanner.d.ts.map +1 -1
  60. package/dist/engine/scanner.js +17 -1
  61. package/dist/fs/paths.d.ts +4 -0
  62. package/dist/fs/paths.d.ts.map +1 -1
  63. package/dist/fs/paths.js +18 -1
  64. package/dist/providers/ceiling/registry.d.ts +1 -0
  65. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  66. package/dist/providers/ceiling/registry.js +12 -5
  67. package/dist/providers/codex/codec/catalog.d.ts +14 -0
  68. package/dist/providers/codex/codec/catalog.d.ts.map +1 -0
  69. package/dist/providers/codex/codec/catalog.js +21 -0
  70. package/dist/providers/codex/codec/materialize.d.ts.map +1 -1
  71. package/dist/providers/codex/codec/materialize.js +6 -5
  72. package/dist/providers/codex/codec/shared.d.ts +19 -0
  73. package/dist/providers/codex/codec/shared.d.ts.map +1 -1
  74. package/dist/providers/codex/codec/shared.js +98 -5
  75. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  76. package/dist/providers/codex/codec/sync-extension.js +142 -32
  77. package/dist/providers/identity/stamp.d.ts.map +1 -1
  78. package/dist/providers/identity/stamp.js +4 -0
  79. package/dist/shared/types.d.ts +1 -0
  80. package/dist/shared/types.d.ts.map +1 -1
  81. package/dist/shared/types.js +4 -0
  82. package/package.json +2 -2
@@ -5,13 +5,13 @@ import { getFrontmatterBlock } from '../../shared/frontmatter.js';
5
5
  import { readGlobalOptions } from '../../shared/shared.utils.js';
6
6
  import { compileDispatchPolicyPreset, } from '../../../config/dispatch-ceiling-preset.js';
7
7
  import { getDispatchPolicyChoices, managedDispatchPolicyValueList, renderDispatchPolicyChoicesMarkdown, } from '../../../config/dispatch-policy-options.js';
8
- import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_DISPATCH_MATRIX_TIERS, VALID_MANAGED_DISPATCH_POLICIES, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
8
+ import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_DISPATCH_MATRIX_TIERS, VALID_MANAGED_DISPATCH_POLICIES, isWorkflowDispatchCandidateLadder, isWorkflowDispatchFallbackRoute, toWorkflowDispatchCandidateLadder, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
9
9
  import { resolveEffectiveConfig, } from '../../../config/resolve.js';
10
10
  import { resolveProjectRoot } from '../../../fs/paths.js';
11
11
  import TOML from '@iarna/toml';
12
- import { getCeilingAdapter, } from '../../../providers/ceiling/registry.js';
12
+ import { getCeilingAdapter, isDirectDispatchRoleName, CLAUDE_TIER_ORDER, } from '../../../providers/ceiling/registry.js';
13
13
  import { classifyModelFamily, } from '../../../providers/identity/family.js';
14
- import { Command } from 'commander';
14
+ import { Command, Option } from 'commander';
15
15
  import YAML from 'yaml';
16
16
  const CODEX_VALUES = [
17
17
  ...VALID_CODEX_DISPATCH_CEILINGS,
@@ -85,6 +85,8 @@ function sourceLabel(source) {
85
85
  return 'environment';
86
86
  case 'project-state':
87
87
  return 'project state';
88
+ case 'invocation':
89
+ return 'explicit invocation';
88
90
  default:
89
91
  return 'none';
90
92
  }
@@ -151,11 +153,7 @@ function normalizeProjectMatrixRouteTarget(value) {
151
153
  }
152
154
  return Object.keys(target).length > 0 ? target : undefined;
153
155
  }
154
- function normalizeProjectMatrixCell(provider, value) {
155
- const bareValue = normalizeProjectMatrixBareValue(provider, value);
156
- if (bareValue !== undefined) {
157
- return bareValue;
158
- }
156
+ function normalizeProjectMatrixRoute(provider, value) {
159
157
  if (!Array.isArray(value) || value.length === 0) {
160
158
  return undefined;
161
159
  }
@@ -173,6 +171,34 @@ function normalizeProjectMatrixCell(provider, value) {
173
171
  }
174
172
  return route.length > 0 ? route : undefined;
175
173
  }
174
+ function normalizeProjectMatrixCandidate(provider, value) {
175
+ const bareValue = normalizeProjectMatrixBareValue(provider, value);
176
+ if (bareValue !== undefined) {
177
+ return bareValue;
178
+ }
179
+ if (isWorkflowDispatchFallbackRoute(value)) {
180
+ const route = normalizeProjectMatrixRoute(provider, value.route);
181
+ return route ? { route } : undefined;
182
+ }
183
+ return normalizeProjectMatrixRouteTarget(value);
184
+ }
185
+ function normalizeProjectMatrixCell(provider, value) {
186
+ const bareValue = normalizeProjectMatrixBareValue(provider, value);
187
+ if (bareValue !== undefined) {
188
+ return bareValue;
189
+ }
190
+ if (isWorkflowDispatchCandidateLadder(value)) {
191
+ const candidates = [];
192
+ for (const candidate of value.candidates) {
193
+ const normalized = normalizeProjectMatrixCandidate(provider, candidate);
194
+ if (normalized !== undefined) {
195
+ candidates.push(normalized);
196
+ }
197
+ }
198
+ return candidates.length > 0 ? { candidates } : undefined;
199
+ }
200
+ return normalizeProjectMatrixRoute(provider, value);
201
+ }
176
202
  function normalizeProjectMatrixProviderValue(provider, value) {
177
203
  const bareValue = normalizeProjectMatrixBareValue(provider, value);
178
204
  if (bareValue !== undefined) {
@@ -343,6 +369,19 @@ function resolveProviderCellFromValue(provider, providerValue, tier, cellSource,
343
369
  warnings: [],
344
370
  };
345
371
  }
372
+ if (isWorkflowDispatchCandidateLadder(cell)) {
373
+ const ceiling = cell.candidates.at(-1);
374
+ if (ceiling === undefined) {
375
+ return null;
376
+ }
377
+ if (typeof ceiling === 'string') {
378
+ return resolveRouteMatrixCell(provider, [ceiling], escalationLevel, cellSource);
379
+ }
380
+ const route = isWorkflowDispatchFallbackRoute(ceiling)
381
+ ? ceiling.route
382
+ : [ceiling];
383
+ return resolveRouteMatrixCell(provider, route, escalationLevel, cellSource);
384
+ }
346
385
  return resolveRouteMatrixCell(provider, cell, escalationLevel, cellSource);
347
386
  }
348
387
  function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix, escalationLevel) {
@@ -370,6 +409,212 @@ function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix
370
409
  }
371
410
  return selected;
372
411
  }
412
+ function resolveProviderMatrixCellDefinition(provider, tier, resolvedConfig, projectMatrix) {
413
+ let selected = null;
414
+ const layers = [
415
+ {
416
+ source: 'user-config',
417
+ providers: resolvedConfig.user.workflow?.dispatchCeiling?.providers,
418
+ },
419
+ {
420
+ source: 'repo-config',
421
+ providers: resolvedConfig.shared.workflow?.dispatchCeiling?.providers,
422
+ },
423
+ {
424
+ source: 'local-config',
425
+ providers: resolvedConfig.local.workflow?.dispatchCeiling?.providers,
426
+ },
427
+ { source: 'project-state', providers: projectMatrix },
428
+ ];
429
+ for (const layer of layers) {
430
+ const providerValue = layer.providers?.[provider];
431
+ if (typeof providerValue === 'string') {
432
+ selected = null;
433
+ continue;
434
+ }
435
+ const cell = providerValue?.[tier];
436
+ if (cell !== undefined) {
437
+ selected = { cell, cellSource: layer.source };
438
+ }
439
+ }
440
+ return selected;
441
+ }
442
+ function candidateRoute(candidate) {
443
+ return isWorkflowDispatchFallbackRoute(candidate)
444
+ ? candidate.route
445
+ : [candidate];
446
+ }
447
+ function candidatePrimaryTarget(provider, tier, candidate) {
448
+ const route = candidateRoute(candidate);
449
+ const entry = route[0];
450
+ if (entry === undefined) {
451
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: fallback routes cannot be empty.`);
452
+ }
453
+ if (typeof entry !== 'string') {
454
+ const validation = validateDispatchRouteTarget(provider, entry);
455
+ if (!validation.valid) {
456
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: ${validation.reason}`);
457
+ }
458
+ }
459
+ const target = typeof entry === 'string'
460
+ ? routeTargetFromBareValue(provider, entry, 0, route.length)
461
+ : routeTargetFromObject(provider, entry, 0, route.length);
462
+ if (target.model && isDirectDispatchRoleName(target.model)) {
463
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: direct dispatch role names are not candidate models.`);
464
+ }
465
+ const targetAdapter = getCeilingAdapter(target.harness);
466
+ if (targetAdapter.mechanism === 'pinned-variant' &&
467
+ (!target.model ||
468
+ !target.effort ||
469
+ !CODEX_VALUES.includes(target.effort))) {
470
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: Codex candidates require a model and supported effort.`);
471
+ }
472
+ if (targetAdapter.mechanism === 'model-arg' && !target.model) {
473
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: model-argument candidates require a model.`);
474
+ }
475
+ return target;
476
+ }
477
+ function candidateTargetKey(target) {
478
+ return JSON.stringify([
479
+ target.harness,
480
+ target.model ?? null,
481
+ target.effort ?? null,
482
+ ]);
483
+ }
484
+ function assertCandidateOrder(provider, tier, ladder) {
485
+ if (ladder.candidates.length === 0) {
486
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: candidates cannot be empty.`);
487
+ }
488
+ const targets = [];
489
+ const seen = new Set();
490
+ let previousClaudeRank = null;
491
+ const codexRanksByModel = new Map();
492
+ for (const candidate of ladder.candidates) {
493
+ const target = candidatePrimaryTarget(provider, tier, candidate);
494
+ const key = candidateTargetKey(target);
495
+ if (seen.has(key)) {
496
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: duplicate candidate ${key}.`);
497
+ }
498
+ seen.add(key);
499
+ if (target.harness === 'claude' && target.model) {
500
+ const rank = CLAUDE_TIER_ORDER.indexOf(target.model);
501
+ if (rank < 0) {
502
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: unsupported Claude model ${JSON.stringify(target.model)}.`);
503
+ }
504
+ if (previousClaudeRank !== null && rank < previousClaudeRank) {
505
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: Claude candidates must be nondecreasing.`);
506
+ }
507
+ previousClaudeRank = rank;
508
+ }
509
+ if (target.harness === 'codex' && target.model && target.effort) {
510
+ const rank = CODEX_VALUES.indexOf(target.effort);
511
+ const previousRank = codexRanksByModel.get(target.model);
512
+ if (previousRank !== undefined && rank < previousRank) {
513
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: Codex efforts for ${target.model} must be nondecreasing.`);
514
+ }
515
+ codexRanksByModel.set(target.model, rank);
516
+ }
517
+ targets.push(target);
518
+ }
519
+ return targets;
520
+ }
521
+ function assertTierCeilingsNondecreasing(provider, previous, current, tier) {
522
+ if (!previous || previous.harness !== current.harness) {
523
+ return;
524
+ }
525
+ if (current.harness === 'claude' &&
526
+ previous.model &&
527
+ current.model &&
528
+ CLAUDE_TIER_ORDER.indexOf(current.model) <
529
+ CLAUDE_TIER_ORDER.indexOf(previous.model)) {
530
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: named tier ceilings must be nondecreasing.`);
531
+ }
532
+ if (current.harness === 'codex' &&
533
+ previous.model === current.model &&
534
+ previous.effort &&
535
+ current.effort &&
536
+ CODEX_VALUES.indexOf(current.effort) <
537
+ CODEX_VALUES.indexOf(previous.effort)) {
538
+ throw new Error(`Malformed ${provider} candidate ordering in ${tier}: named tier ceilings must be nondecreasing.`);
539
+ }
540
+ }
541
+ function requestedCandidateMatches(provider, requested, target) {
542
+ if (target.harness !== provider || target.model !== requested.model) {
543
+ return false;
544
+ }
545
+ return provider !== 'codex' || target.effort === requested.effort;
546
+ }
547
+ function formatRequestedCandidate(provider, requested) {
548
+ return provider === 'codex'
549
+ ? `${requested.model}/${requested.effort}`
550
+ : requested.model;
551
+ }
552
+ function resolveRequestedMatrixCandidate(provider, requested, ceilingTier, resolvedConfig, projectMatrix, escalationLevel) {
553
+ const ceilingIndex = ceilingTier === null
554
+ ? VALID_DISPATCH_MATRIX_TIERS.length - 1
555
+ : VALID_DISPATCH_MATRIX_TIERS.indexOf(ceilingTier);
556
+ let ceilingCellFound = ceilingTier === null;
557
+ let previousCeiling = null;
558
+ let allowedMatch;
559
+ let foundAboveCeiling = false;
560
+ for (const [tierIndex, tier] of VALID_DISPATCH_MATRIX_TIERS.entries()) {
561
+ const definition = resolveProviderMatrixCellDefinition(provider, tier, resolvedConfig, projectMatrix);
562
+ if (!definition) {
563
+ continue;
564
+ }
565
+ if (tier === ceilingTier) {
566
+ ceilingCellFound = true;
567
+ }
568
+ const ladder = toWorkflowDispatchCandidateLadder(definition.cell);
569
+ const targets = assertCandidateOrder(provider, tier, ladder);
570
+ const currentCeiling = targets.at(-1);
571
+ assertTierCeilingsNondecreasing(provider, previousCeiling, currentCeiling, tier);
572
+ previousCeiling = currentCeiling;
573
+ for (const [candidateIndex, target] of targets.entries()) {
574
+ if (!requestedCandidateMatches(provider, requested, target)) {
575
+ continue;
576
+ }
577
+ const candidate = ladder.candidates[candidateIndex];
578
+ const routeSignature = JSON.stringify(candidateRoute(candidate));
579
+ if (tierIndex > ceilingIndex) {
580
+ foundAboveCeiling = true;
581
+ continue;
582
+ }
583
+ if (allowedMatch && allowedMatch.routeSignature !== routeSignature) {
584
+ throw new Error(`Ambiguous ${provider} candidate ${formatRequestedCandidate(provider, requested)} is configured with multiple routes at or below the ${ceilingTier ?? 'uncapped'} ceiling.`);
585
+ }
586
+ allowedMatch ??= {
587
+ tier,
588
+ candidate,
589
+ cellSource: definition.cellSource,
590
+ routeSignature,
591
+ };
592
+ }
593
+ }
594
+ if (!ceilingCellFound) {
595
+ throw new Error(`Exact candidate selection requires a configured ${ceilingTier} candidate ladder for ${provider}.`);
596
+ }
597
+ if (!allowedMatch) {
598
+ const label = formatRequestedCandidate(provider, requested);
599
+ if (foundAboveCeiling) {
600
+ throw new Error(`${provider} candidate ${label} is above the configured ${ceilingTier} ceiling.`);
601
+ }
602
+ throw new Error(`${provider} candidate ${label} is not present in the configured ${provider} candidate ladders.`);
603
+ }
604
+ const resolution = resolveRouteMatrixCell(provider, candidateRoute(allowedMatch.candidate), escalationLevel, allowedMatch.cellSource);
605
+ if (!resolution || resolution.value === null) {
606
+ throw new Error(`Configured ${provider} candidate ${formatRequestedCandidate(provider, requested)} cannot compile to an exact dispatch target.`);
607
+ }
608
+ return {
609
+ resolution: {
610
+ ...resolution,
611
+ selectionBranch: resolution.selectionBranch === 'escalation-target'
612
+ ? 'escalation-target'
613
+ : 'candidate-requested',
614
+ },
615
+ candidateTier: allowedMatch.tier,
616
+ };
617
+ }
373
618
  function readProjectDispatchPolicy(provider, content) {
374
619
  const frontmatter = getFrontmatterBlock(content);
375
620
  if (!frontmatter) {
@@ -645,7 +890,13 @@ function readResolvedConfigCeiling(provider, resolvedConfig) {
645
890
  return stripConfigCandidateSource(winner);
646
891
  }
647
892
  function normalizeRole(value) {
648
- return value === 'reviewer' ? 'reviewer' : 'implementer';
893
+ if (value === undefined) {
894
+ return 'implementer';
895
+ }
896
+ if (value === 'implementer' || value === 'reviewer') {
897
+ return value;
898
+ }
899
+ throw new Error(`Invalid dispatch role ${JSON.stringify(value)}. Expected implementer or reviewer.`);
649
900
  }
650
901
  function providerValueOrder(provider) {
651
902
  if (provider === 'codex') {
@@ -674,6 +925,55 @@ function normalizePreferredValue(provider, value) {
674
925
  }
675
926
  return normalized;
676
927
  }
928
+ function normalizeCeilingTier(value, role) {
929
+ if (value === undefined) {
930
+ return null;
931
+ }
932
+ const normalized = value.trim();
933
+ if (!VALID_DISPATCH_MATRIX_TIERS.includes(normalized)) {
934
+ throw new Error(`Invalid invocation ceiling tier "${normalized}". Valid tiers: ${VALID_DISPATCH_MATRIX_TIERS.join(', ')}.`);
935
+ }
936
+ if (role === 'reviewer') {
937
+ throw new Error('Invocation ceiling tiers are only supported for implementer/fix task dispatch; reviewers use the configured review ceiling.');
938
+ }
939
+ return normalized;
940
+ }
941
+ function normalizeRequestedCandidate(provider, role, options) {
942
+ const model = options.candidateModel?.trim() ?? '';
943
+ const effort = options.candidateEffort?.trim() ?? '';
944
+ if (!model && !effort) {
945
+ return null;
946
+ }
947
+ if (options.preferred?.trim()) {
948
+ throw new Error('Exact candidate flags cannot be combined with --preferred; use one selection path.');
949
+ }
950
+ if (role === 'reviewer') {
951
+ throw new Error('Reviewer candidate requests are not supported; reviewers use the configured review ceiling.');
952
+ }
953
+ if (!model) {
954
+ throw new Error('--candidate-model is required for an exact candidate.');
955
+ }
956
+ if (isDirectDispatchRoleName(model)) {
957
+ throw new Error('Direct dispatch role names are not candidate models; request the configured provider model and effort instead.');
958
+ }
959
+ if (provider === 'codex') {
960
+ if (!effort) {
961
+ throw new Error('--candidate-effort is required for an exact Codex candidate.');
962
+ }
963
+ if (!CODEX_VALUES.includes(effort)) {
964
+ throw new Error(`Invalid Codex candidate effort "${effort}". Valid values: ${CODEX_VALUES.join(', ')}.`);
965
+ }
966
+ return { model, effort };
967
+ }
968
+ if (effort) {
969
+ throw new Error(`--candidate-effort is only valid for Codex; ${provider} candidates use --candidate-model only.`);
970
+ }
971
+ if (provider === 'claude' &&
972
+ !CLAUDE_VALUES.includes(model)) {
973
+ throw new Error(`Invalid Claude candidate model "${model}". Valid values: ${CLAUDE_VALUES.join(', ')}.`);
974
+ }
975
+ return { model };
976
+ }
677
977
  function normalizeEscalationLevel(value) {
678
978
  if (value === undefined) {
679
979
  return 0;
@@ -696,6 +996,10 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
696
996
  policy: policy.policy,
697
997
  cellSource: policy.cellSource,
698
998
  target: policy.target,
999
+ requestedCandidate: policy.requestedCandidate ?? null,
1000
+ candidateTier: policy.candidateTier ?? null,
1001
+ ceilingTier: policy.ceilingTier ?? policyTier(policy.policy),
1002
+ ceilingTarget: policy.ceilingTarget ?? policy.target,
699
1003
  };
700
1004
  if (policy.mode === 'inherit') {
701
1005
  return {
@@ -709,6 +1013,21 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
709
1013
  target: null,
710
1014
  };
711
1015
  }
1016
+ if (policy.requestedCandidate) {
1017
+ const selectedValue = policy.target
1018
+ ? dispatchValueFromRouteTarget(policy.target)
1019
+ : null;
1020
+ return {
1021
+ ...baseSelection,
1022
+ preferredValue: null,
1023
+ selectedValue,
1024
+ capped: false,
1025
+ selectionMode: selectedValue ? 'candidate' : 'unresolved',
1026
+ selectionBranch: policy.selectionBranch,
1027
+ family: selectionFamily(provider, selectedValue, policy.target),
1028
+ target: policy.target,
1029
+ };
1030
+ }
712
1031
  if (policy.policy === 'uncapped') {
713
1032
  const targetValue = policy.target
714
1033
  ? dispatchValueFromRouteTarget(policy.target)
@@ -769,6 +1088,10 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
769
1088
  }
770
1089
  const selectedIndex = Math.min(preferredIndex, ceilingIndex);
771
1090
  const selectedValue = order[selectedIndex];
1091
+ const targetValue = policy.target
1092
+ ? dispatchValueFromRouteTarget(policy.target)
1093
+ : null;
1094
+ const selectedTarget = targetValue === selectedValue ? policy.target : null;
772
1095
  return {
773
1096
  ...baseSelection,
774
1097
  preferredValue,
@@ -776,8 +1099,8 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
776
1099
  capped: preferredIndex > ceilingIndex,
777
1100
  selectionMode: 'capped',
778
1101
  selectionBranch: policy.selectionBranch,
779
- family: selectionFamily(provider, selectedValue, selectedValue === policy.value ? policy.target : null),
780
- target: selectedValue === policy.value ? policy.target : null,
1102
+ family: selectionFamily(provider, selectedValue, selectedTarget),
1103
+ target: selectedTarget,
781
1104
  };
782
1105
  }
783
1106
  function hasCodexVariantDispatchArgs(dispatchArgs) {
@@ -835,6 +1158,10 @@ function buildProviderResolution(provider, policy, role, orchestratorTier, prefe
835
1158
  selection: {
836
1159
  role,
837
1160
  preferredValue,
1161
+ requestedCandidate: null,
1162
+ candidateTier: null,
1163
+ ceilingTier: null,
1164
+ ceilingTarget: null,
838
1165
  selectedValue: null,
839
1166
  capped: false,
840
1167
  selectionMode: 'unresolved',
@@ -933,9 +1260,11 @@ async function resolveDispatchCeiling(context, dependencies, options) {
933
1260
  const providerDefaultEffort = provider === 'codex'
934
1261
  ? await resolveCodexProviderDefaultEffort(repoRoot, context, dependencies)
935
1262
  : 'not-applicable';
1263
+ const requestedCandidate = normalizeRequestedCandidate(provider, role, options);
936
1264
  const preferredValue = normalizePreferredValue(provider, options.preferred);
1265
+ const ceilingTier = normalizeCeilingTier(options.ceilingTier, role);
937
1266
  const escalationLevel = normalizeEscalationLevel(options.escalationLevel);
938
- const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue);
1267
+ const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue, requestedCandidate, ceilingTier);
939
1268
  for (const warning of resolvedValue?.warnings ?? []) {
940
1269
  context.logger.warn(warning);
941
1270
  }
@@ -944,6 +1273,32 @@ async function resolveDispatchCeiling(context, dependencies, options) {
944
1273
  [provider]: providerResolution,
945
1274
  };
946
1275
  if (resolvedValue) {
1276
+ const incompleteManagedPreflight = options.preflight === true &&
1277
+ resolvedValue.mode === 'managed' &&
1278
+ providerResolution.mode === 'advisory' &&
1279
+ providerResolution.selection.target?.crossHarness !== true &&
1280
+ providerResolution.selection.selectionMode !== 'no-review-target';
1281
+ if (incompleteManagedPreflight) {
1282
+ const shouldBlock = options.nonInteractive === true ||
1283
+ isNonInteractiveEnv(dependencies.processEnv) ||
1284
+ (!context.interactive && !context.json);
1285
+ const message = shouldBlock ? blockMessage(provider) : undefined;
1286
+ return {
1287
+ status: shouldBlock ? 'blocked' : 'unresolved',
1288
+ provider,
1289
+ value: resolvedValue.value,
1290
+ policyMode: resolvedValue.mode,
1291
+ policy: resolvedValue.policy,
1292
+ source: resolvedValue.source,
1293
+ preset: resolvedValue.preset,
1294
+ unresolved: true,
1295
+ projectPath,
1296
+ providerDefaultEffort,
1297
+ matrix: resolvedValue.matrix,
1298
+ providers,
1299
+ message,
1300
+ };
1301
+ }
947
1302
  return {
948
1303
  status: 'resolved',
949
1304
  provider,
@@ -985,16 +1340,61 @@ async function resolveDispatchCeiling(context, dependencies, options) {
985
1340
  * Never reads the preset label for dispatch — the preset is surfaced as
986
1341
  * provenance only.
987
1342
  */
988
- async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue) {
1343
+ async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue, requestedCandidate, invocationCeilingTier) {
989
1344
  const configCeiling = readResolvedConfigCeiling(provider, resolvedConfig);
990
1345
  const projectCeiling = await resolveProjectStateCeiling(provider, projectPath, dependencies);
991
- const baseCeiling = configCeiling ?? projectCeiling;
1346
+ let baseCeiling = configCeiling ?? projectCeiling;
992
1347
  if (!baseCeiling) {
993
1348
  return null;
994
1349
  }
1350
+ if (invocationCeilingTier) {
1351
+ baseCeiling = {
1352
+ ...baseCeiling,
1353
+ mode: 'managed',
1354
+ policy: invocationCeilingTier,
1355
+ value: compiledPolicyValueForProvider(provider, compileDispatchPolicyPreset(invocationCeilingTier)),
1356
+ source: 'invocation',
1357
+ preset: invocationCeilingTier,
1358
+ matrix: projectCeiling?.matrix ?? baseCeiling.matrix,
1359
+ cellSource: null,
1360
+ target: null,
1361
+ selectionBranch: 'prompt-persisted',
1362
+ ceilingTier: invocationCeilingTier,
1363
+ ceilingTarget: null,
1364
+ };
1365
+ }
995
1366
  if (baseCeiling.mode === 'inherit') {
1367
+ if (requestedCandidate) {
1368
+ throw new Error('Exact candidate selection requires a managed dispatch policy and configured candidate ladder.');
1369
+ }
996
1370
  return baseCeiling;
997
1371
  }
1372
+ if (requestedCandidate) {
1373
+ if (baseCeiling.policy === 'legacy-ceiling') {
1374
+ throw new Error('Exact candidate selection requires a configured candidate ladder; legacy scalar dispatch ceilings support --preferred only during migration.');
1375
+ }
1376
+ const ceilingTier = policyTier(baseCeiling.policy);
1377
+ const selected = resolveRequestedMatrixCandidate(provider, requestedCandidate, ceilingTier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel);
1378
+ const ceilingCell = ceilingTier
1379
+ ? resolveProviderMatrixCell(provider, ceilingTier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel)
1380
+ : null;
1381
+ return {
1382
+ ...baseCeiling,
1383
+ value: ceilingCell?.value ?? baseCeiling.value,
1384
+ cellSource: selected.resolution.cellSource,
1385
+ target: selected.resolution.target,
1386
+ selectionBranch: selected.resolution.selectionBranch,
1387
+ warnings: [
1388
+ ...baseCeiling.warnings,
1389
+ ...(ceilingCell?.warnings ?? []),
1390
+ ...selected.resolution.warnings,
1391
+ ],
1392
+ requestedCandidate,
1393
+ candidateTier: selected.candidateTier,
1394
+ ceilingTarget: ceilingCell?.target ?? null,
1395
+ ...(ceilingTier ? { ceilingTier } : {}),
1396
+ };
1397
+ }
998
1398
  if (baseCeiling.policy === 'uncapped') {
999
1399
  const preferredTier = uncappedPreferredTier(provider, role, preferredValue);
1000
1400
  if (preferredTier) {
@@ -1014,6 +1414,30 @@ async function resolveCeilingValue(provider, resolvedConfig, projectPath, depend
1014
1414
  const tier = policyTier(baseCeiling.policy);
1015
1415
  const matrixCell = resolveProviderMatrixCell(provider, tier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel);
1016
1416
  if (matrixCell) {
1417
+ if (provider === 'codex' &&
1418
+ role === 'implementer' &&
1419
+ preferredValue !== null &&
1420
+ matrixCell.value !== null) {
1421
+ const order = providerValueOrder(provider);
1422
+ const preferredIndex = order?.indexOf(preferredValue) ?? -1;
1423
+ const ceilingIndex = order?.indexOf(matrixCell.value) ?? -1;
1424
+ if (order && preferredIndex >= 0 && ceilingIndex >= 0) {
1425
+ const selectedValue = order[Math.min(preferredIndex, ceilingIndex)];
1426
+ if (selectedValue !== matrixCell.value) {
1427
+ const selectedTarget = matrixCell.target?.harness === 'codex'
1428
+ ? { ...matrixCell.target, effort: selectedValue }
1429
+ : matrixCell.target;
1430
+ return {
1431
+ ...baseCeiling,
1432
+ value: matrixCell.value,
1433
+ cellSource: matrixCell.cellSource,
1434
+ target: selectedTarget,
1435
+ selectionBranch: matrixCell.selectionBranch,
1436
+ warnings: [...baseCeiling.warnings, ...matrixCell.warnings],
1437
+ };
1438
+ }
1439
+ }
1440
+ }
1017
1441
  return {
1018
1442
  ...baseCeiling,
1019
1443
  value: matrixCell.value,
@@ -1049,6 +1473,15 @@ function writeHumanResolution(context, resolution) {
1049
1473
  if (providerResolution) {
1050
1474
  context.logger.info(`Mode: ${providerResolution.mode} (${providerResolution.mechanism})`);
1051
1475
  context.logger.info(`Selection: ${providerResolution.selection.selectionMode}`);
1476
+ if (providerResolution.selection.requestedCandidate) {
1477
+ const requested = providerResolution.selection.requestedCandidate;
1478
+ context.logger.info(`Requested candidate: model=${requested.model}${requested.effort ? ` effort=${requested.effort}` : ''}`);
1479
+ context.logger.info(`Candidate tier: ${providerResolution.selection.candidateTier ?? 'none'}; ceiling tier: ${providerResolution.selection.ceilingTier ?? 'uncapped'}`);
1480
+ const ceilingTarget = providerResolution.selection.ceilingTarget;
1481
+ if (ceilingTarget) {
1482
+ context.logger.info(`Effective ceiling target: model=${ceilingTarget.model ?? 'none'}${ceilingTarget.effort ? ` effort=${ceilingTarget.effort}` : ''}`);
1483
+ }
1484
+ }
1052
1485
  if (providerResolution.target) {
1053
1486
  const details = [
1054
1487
  `harness=${providerResolution.target.harness}`,
@@ -1159,9 +1592,12 @@ export function createProjectDispatchCeilingCommand(overrides = {}) {
1159
1592
  command.addCommand(new Command('resolve')
1160
1593
  .description('Resolve dispatch policy for a provider')
1161
1594
  .requiredOption('--provider <provider>', 'Provider name: codex, claude, or cursor are enforced; unregistered providers resolve as unsupported advisory')
1162
- .option('--role <role>', 'Dispatch role for variant compilation: implementer (default) or reviewer')
1595
+ .addOption(new Option('--role <role>', 'Dispatch role for variant compilation: implementer (default) or reviewer').choices(['implementer', 'reviewer']))
1163
1596
  .option('--orchestrator-tier <tier>', 'Orchestrator tier, used to flag verify-on-upgrade for above-orchestrator requests')
1164
- .option('--preferred <value>', 'Preferred implementer/fix dispatch value before applying the resolved policy')
1597
+ .option('--preferred <value>', 'Legacy preferred implementer/fix value before applying the resolved policy')
1598
+ .addOption(new Option('--ceiling-tier <tier>', 'Invocation-only named maximum tier; never persists configuration or project state').choices([...VALID_DISPATCH_MATRIX_TIERS]))
1599
+ .option('--candidate-model <model>', 'Exact configured implementer candidate model beneath the named ceiling')
1600
+ .option('--candidate-effort <effort>', 'Exact configured Codex candidate effort paired with --candidate-model')
1165
1601
  .option('--escalation-level <level>', 'Ordered route entry to select; 0 is the floor and higher values advance through escalation targets')
1166
1602
  .option('--project-path <path>', 'Read project-state policy from an explicit project path')
1167
1603
  .option('--preflight', 'Treat unresolved non-interactive resolution as an implementation block')
@@ -1 +1 @@
1
- {"version":3,"file":"materialize.d.ts","sourceRoot":"","sources":["../../../../src/commands/providers/codex/materialize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,sBAAsB,EACtB,qCAAqC,EACtC,MAAM,qCAAqC,CAAC;AAS7C,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAsD5C,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAaR;AAwGD,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,qCAAqC,CAAM,GAC7D,OAAO,CAoCT"}
1
+ {"version":3,"file":"materialize.d.ts","sourceRoot":"","sources":["../../../../src/commands/providers/codex/materialize.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,sBAAsB,EACtB,qCAAqC,EACtC,MAAM,qCAAqC,CAAC;AAU7C,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAsD5C,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAaR;AA+GD,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,qCAAqC,CAAM,GAC7D,OAAO,CAoCT"}
@@ -8,6 +8,7 @@ import { ensureDir, fileExists } from '../../../fs/io.js';
8
8
  import { resolveProjectRoot, resolveScopeRoot } from '../../../fs/paths.js';
9
9
  import { mergeCodexConfigForRole } from '../../../providers/codex/codec/config-merge.js';
10
10
  import { materializeCodexRole } from '../../../providers/codex/codec/materialize.js';
11
+ import { withOatManagedCodexRoleOwner } from '../../../providers/codex/codec/shared.js';
11
12
  import { Command, Option } from 'commander';
12
13
  function createDependencies() {
13
14
  return {
@@ -59,12 +60,16 @@ async function buildCodexMaterializePlan(agentName, options, context, dependenci
59
60
  const scopeRoot = await dependencies.resolveScopeRoot(scope, context);
60
61
  const agentPath = resolveAgentPath(agentName, options, scopeRoot);
61
62
  const agent = await dependencies.parseCanonicalAgentFile(agentPath);
62
- const role = dependencies.materializeCodexRole({
63
+ const materializedRole = dependencies.materializeCodexRole({
63
64
  agent,
64
65
  model,
65
66
  effort,
66
67
  roleName: options.roleName,
67
68
  });
69
+ const role = {
70
+ ...materializedRole,
71
+ content: withOatManagedCodexRoleOwner(materializedRole.content, scope === 'user' ? 'user-config' : 'project-config'),
72
+ };
68
73
  const rolePath = join(scopeRoot, '.codex', role.configFile);
69
74
  const configPath = join(scopeRoot, '.codex', 'config.toml');
70
75
  return {
@@ -9,6 +9,10 @@ export declare function isProjectStatePhase(value: string): value is ProjectStat
9
9
  export declare function isProjectStateFrontmatterField(value: string): value is ProjectStateFrontmatterField;
10
10
  export declare function getFrontmatterBlock(content: string): string | null;
11
11
  export declare function getFrontmatterField(frontmatter: string, field: string): string | null;
12
+ export declare function parseFrontmatterScalarFields(frontmatter: string, fields: readonly string[]): {
13
+ valid: boolean;
14
+ values: Record<string, string>;
15
+ };
12
16
  /**
13
17
  * Parse an `oat_generated_at` value into a comparable epoch millisecond time.
14
18
  *
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/frontmatter.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,6CAA8C,CAAC;AAE/E,eAAO,MAAM,oBAAoB,gFAOvB,CAAC;AAEX,eAAO,MAAM,gCAAgC,sgBA2BnC,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACpE,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,4BAA4B,GACtC,CAAC,OAAO,gCAAgC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,4BAA4B,CAIvC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAKf;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQxB;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB"}
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/frontmatter.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,mBAAmB,6CAA8C,CAAC;AAE/E,eAAO,MAAM,oBAAoB,gFAOvB,CAAC;AAEX,eAAO,MAAM,gCAAgC,sgBA2BnC,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACpE,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,4BAA4B,GACtC,CAAC,OAAO,gCAAgC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,4BAA4B,CAIvC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAKf;AAED,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA4BpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQxB;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB"}