@open-agent-toolkit/cli 0.1.45 → 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 (112) hide show
  1. package/assets/agents/oat-phase-implementer.md +202 -191
  2. package/assets/agents/oat-reviewer.md +12 -2
  3. package/assets/config/dispatch-matrix-recommendation.json +120 -13
  4. package/assets/docs/cli-utilities/config-and-local-state.md +7 -0
  5. package/assets/docs/cli-utilities/configuration.md +183 -84
  6. package/assets/docs/cli-utilities/workflow-gates.md +161 -27
  7. package/assets/docs/contributing/skills.md +14 -8
  8. package/assets/docs/provider-sync/config.md +5 -1
  9. package/assets/docs/provider-sync/manifest-and-drift.md +6 -1
  10. package/assets/docs/provider-sync/providers.md +42 -5
  11. package/assets/docs/provider-sync/scope-and-surface.md +3 -2
  12. package/assets/docs/reference/cli-reference.md +3 -1
  13. package/assets/docs/reference/oat-directory-structure.md +27 -26
  14. package/assets/docs/workflows/projects/artifacts.md +31 -1
  15. package/assets/docs/workflows/projects/dispatch-ceiling.md +235 -174
  16. package/assets/docs/workflows/projects/implementation-execution.md +283 -253
  17. package/assets/docs/workflows/projects/lifecycle.md +26 -5
  18. package/assets/docs/workflows/projects/reviews.md +27 -2
  19. package/assets/migration/pjm-restructure.md +1 -1
  20. package/assets/public-package-versions.json +4 -4
  21. package/assets/skills/oat-project-implement/SKILL.md +347 -178
  22. package/assets/skills/oat-project-import-plan/SKILL.md +173 -16
  23. package/assets/skills/oat-project-next/SKILL.md +2 -2
  24. package/assets/skills/oat-project-plan/SKILL.md +122 -92
  25. package/assets/skills/oat-project-plan-writing/SKILL.md +246 -15
  26. package/assets/skills/oat-project-quick-start/SKILL.md +156 -94
  27. package/assets/skills/oat-project-review-provide/SKILL.md +94 -22
  28. package/dist/commands/config/index.d.ts +2 -2
  29. package/dist/commands/config/index.d.ts.map +1 -1
  30. package/dist/commands/config/index.js +129 -53
  31. package/dist/commands/doctor/index.d.ts +2 -2
  32. package/dist/commands/doctor/index.d.ts.map +1 -1
  33. package/dist/commands/doctor/index.js +102 -31
  34. package/dist/commands/gate/index.d.ts +7 -1
  35. package/dist/commands/gate/index.d.ts.map +1 -1
  36. package/dist/commands/gate/index.js +544 -67
  37. package/dist/commands/gate/review-verdict.d.ts +16 -0
  38. package/dist/commands/gate/review-verdict.d.ts.map +1 -1
  39. package/dist/commands/gate/review-verdict.js +72 -9
  40. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  41. package/dist/commands/project/dispatch-ceiling/index.js +586 -23
  42. package/dist/commands/providers/codex/index.d.ts +4 -0
  43. package/dist/commands/providers/codex/index.d.ts.map +1 -0
  44. package/dist/commands/providers/codex/index.js +7 -0
  45. package/dist/commands/providers/codex/materialize.d.ts +5 -0
  46. package/dist/commands/providers/codex/materialize.d.ts.map +1 -0
  47. package/dist/commands/providers/codex/materialize.js +157 -0
  48. package/dist/commands/providers/index.d.ts +2 -2
  49. package/dist/commands/providers/index.d.ts.map +1 -1
  50. package/dist/commands/providers/index.js +4 -2
  51. package/dist/commands/providers/providers.types.d.ts +23 -0
  52. package/dist/commands/providers/providers.types.d.ts.map +1 -1
  53. package/dist/commands/shared/codex-strays.d.ts.map +1 -1
  54. package/dist/commands/shared/codex-strays.js +11 -1
  55. package/dist/commands/shared/frontmatter.d.ts +4 -0
  56. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  57. package/dist/commands/shared/frontmatter.js +23 -0
  58. package/dist/commands/status/index.d.ts +5 -1
  59. package/dist/commands/status/index.d.ts.map +1 -1
  60. package/dist/commands/status/index.js +10 -4
  61. package/dist/commands/sync/index.d.ts.map +1 -1
  62. package/dist/commands/sync/index.js +10 -3
  63. package/dist/commands/sync/sync.types.d.ts +5 -1
  64. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  65. package/dist/config/dispatch-ceiling-preset.d.ts +4 -0
  66. package/dist/config/dispatch-ceiling-preset.d.ts.map +1 -1
  67. package/dist/config/dispatch-ceiling-preset.js +3 -0
  68. package/dist/config/dispatch-policy-options.d.ts +20 -0
  69. package/dist/config/dispatch-policy-options.d.ts.map +1 -0
  70. package/dist/config/dispatch-policy-options.js +96 -0
  71. package/dist/config/oat-config.d.ts +25 -2
  72. package/dist/config/oat-config.d.ts.map +1 -1
  73. package/dist/config/oat-config.js +154 -24
  74. package/dist/config/resolve.d.ts +8 -0
  75. package/dist/config/resolve.d.ts.map +1 -1
  76. package/dist/config/resolve.js +66 -2
  77. package/dist/engine/index.d.ts +1 -1
  78. package/dist/engine/index.d.ts.map +1 -1
  79. package/dist/engine/index.js +1 -1
  80. package/dist/engine/scanner.d.ts +1 -0
  81. package/dist/engine/scanner.d.ts.map +1 -1
  82. package/dist/engine/scanner.js +17 -1
  83. package/dist/fs/paths.d.ts +4 -0
  84. package/dist/fs/paths.d.ts.map +1 -1
  85. package/dist/fs/paths.js +18 -1
  86. package/dist/providers/ceiling/registry.d.ts +8 -5
  87. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  88. package/dist/providers/ceiling/registry.js +25 -7
  89. package/dist/providers/codex/codec/catalog.d.ts +14 -0
  90. package/dist/providers/codex/codec/catalog.d.ts.map +1 -0
  91. package/dist/providers/codex/codec/catalog.js +21 -0
  92. package/dist/providers/codex/codec/config-merge.d.ts +6 -0
  93. package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
  94. package/dist/providers/codex/codec/config-merge.js +7 -0
  95. package/dist/providers/codex/codec/materialize.d.ts +16 -0
  96. package/dist/providers/codex/codec/materialize.d.ts.map +1 -0
  97. package/dist/providers/codex/codec/materialize.js +58 -0
  98. package/dist/providers/codex/codec/shared.d.ts +20 -0
  99. package/dist/providers/codex/codec/shared.d.ts.map +1 -1
  100. package/dist/providers/codex/codec/shared.js +107 -6
  101. package/dist/providers/codex/codec/sync-extension.d.ts +7 -1
  102. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  103. package/dist/providers/codex/codec/sync-extension.js +299 -52
  104. package/dist/providers/identity/availability.d.ts +24 -1
  105. package/dist/providers/identity/availability.d.ts.map +1 -1
  106. package/dist/providers/identity/availability.js +253 -19
  107. package/dist/providers/identity/stamp.d.ts.map +1 -1
  108. package/dist/providers/identity/stamp.js +4 -0
  109. package/dist/shared/types.d.ts +1 -0
  110. package/dist/shared/types.d.ts.map +1 -1
  111. package/dist/shared/types.js +4 -0
  112. package/package.json +2 -2
@@ -4,13 +4,14 @@ import { buildCommandContext, } from '../../../app/command-context.js';
4
4
  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
- import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_DISPATCH_MATRIX_TIERS, VALID_MANAGED_DISPATCH_POLICIES, } from '../../../config/oat-config.js';
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, isWorkflowDispatchCandidateLadder, isWorkflowDispatchFallbackRoute, toWorkflowDispatchCandidateLadder, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
8
9
  import { resolveEffectiveConfig, } from '../../../config/resolve.js';
9
10
  import { resolveProjectRoot } from '../../../fs/paths.js';
10
11
  import TOML from '@iarna/toml';
11
- import { getCeilingAdapter, } from '../../../providers/ceiling/registry.js';
12
+ import { getCeilingAdapter, isDirectDispatchRoleName, CLAUDE_TIER_ORDER, } from '../../../providers/ceiling/registry.js';
12
13
  import { classifyModelFamily, } from '../../../providers/identity/family.js';
13
- import { Command } from 'commander';
14
+ import { Command, Option } from 'commander';
14
15
  import YAML from 'yaml';
15
16
  const CODEX_VALUES = [
16
17
  ...VALID_CODEX_DISPATCH_CEILINGS,
@@ -84,6 +85,8 @@ function sourceLabel(source) {
84
85
  return 'environment';
85
86
  case 'project-state':
86
87
  return 'project state';
88
+ case 'invocation':
89
+ return 'explicit invocation';
87
90
  default:
88
91
  return 'none';
89
92
  }
@@ -106,7 +109,7 @@ function isValidManagedPolicy(value) {
106
109
  VALID_MANAGED_DISPATCH_POLICIES.includes(value));
107
110
  }
108
111
  function validManagedPolicyList() {
109
- return VALID_MANAGED_DISPATCH_POLICIES.join(', ');
112
+ return managedDispatchPolicyValueList(', ');
110
113
  }
111
114
  function validProviderValueList(provider) {
112
115
  return providerValueOrder(provider)?.join(', ') ?? 'none';
@@ -150,11 +153,7 @@ function normalizeProjectMatrixRouteTarget(value) {
150
153
  }
151
154
  return Object.keys(target).length > 0 ? target : undefined;
152
155
  }
153
- function normalizeProjectMatrixCell(provider, value) {
154
- const bareValue = normalizeProjectMatrixBareValue(provider, value);
155
- if (bareValue !== undefined) {
156
- return bareValue;
157
- }
156
+ function normalizeProjectMatrixRoute(provider, value) {
158
157
  if (!Array.isArray(value) || value.length === 0) {
159
158
  return undefined;
160
159
  }
@@ -172,6 +171,34 @@ function normalizeProjectMatrixCell(provider, value) {
172
171
  }
173
172
  return route.length > 0 ? route : undefined;
174
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
+ }
175
202
  function normalizeProjectMatrixProviderValue(provider, value) {
176
203
  const bareValue = normalizeProjectMatrixBareValue(provider, value);
177
204
  if (bareValue !== undefined) {
@@ -230,6 +257,22 @@ function policyTier(policy) {
230
257
  ? policy
231
258
  : null;
232
259
  }
260
+ function uncappedPreferredTier(provider, role, preferredValue) {
261
+ if (provider !== 'codex' || role === 'reviewer' || preferredValue === null) {
262
+ return null;
263
+ }
264
+ if (VALID_DISPATCH_MATRIX_TIERS.includes(preferredValue)) {
265
+ return preferredValue;
266
+ }
267
+ let matchedTier = null;
268
+ for (const tier of VALID_DISPATCH_MATRIX_TIERS) {
269
+ const compiledValue = compiledPolicyValueForProvider(provider, compileDispatchPolicyPreset(tier));
270
+ if (compiledValue === preferredValue) {
271
+ matchedTier = tier;
272
+ }
273
+ }
274
+ return matchedTier;
275
+ }
233
276
  function dispatchValueFromRouteTarget(target) {
234
277
  const adapter = getCeilingAdapter(target.harness);
235
278
  if (adapter.mechanism === 'pinned-variant') {
@@ -276,12 +319,25 @@ function resolveRouteMatrixCell(provider, route, escalationLevel, cellSource) {
276
319
  const target = typeof entry === 'string'
277
320
  ? routeTargetFromBareValue(provider, entry, routeIndex, route.length)
278
321
  : routeTargetFromObject(provider, entry, routeIndex, route.length);
279
- const value = typeof entry === 'string' ? entry : dispatchValueFromRouteTarget(target);
322
+ const targetValidation = typeof entry === 'string'
323
+ ? { valid: true }
324
+ : validateDispatchRouteTarget(provider, entry);
325
+ const value = targetValidation.valid
326
+ ? typeof entry === 'string'
327
+ ? entry
328
+ : dispatchValueFromRouteTarget(target)
329
+ : null;
330
+ const warnings = targetValidation.valid
331
+ ? []
332
+ : [
333
+ `Ignoring incomplete Codex dispatch target at route index ${routeIndex}: ${targetValidation.reason}`,
334
+ ];
280
335
  return {
281
336
  value,
282
337
  cellSource,
283
338
  target,
284
339
  selectionBranch: routeIndex > 0 ? 'escalation-target' : 'matrix-pinned',
340
+ warnings,
285
341
  };
286
342
  }
287
343
  function resolveProviderCellFromValue(provider, providerValue, tier, cellSource, escalationLevel) {
@@ -294,6 +350,7 @@ function resolveProviderCellFromValue(provider, providerValue, tier, cellSource,
294
350
  cellSource,
295
351
  target: null,
296
352
  selectionBranch: 'prompt-persisted',
353
+ warnings: [],
297
354
  };
298
355
  }
299
356
  if (tier === null) {
@@ -309,8 +366,22 @@ function resolveProviderCellFromValue(provider, providerValue, tier, cellSource,
309
366
  cellSource,
310
367
  target: null,
311
368
  selectionBranch: 'matrix-pinned',
369
+ warnings: [],
312
370
  };
313
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
+ }
314
385
  return resolveRouteMatrixCell(provider, cell, escalationLevel, cellSource);
315
386
  }
316
387
  function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix, escalationLevel) {
@@ -338,6 +409,212 @@ function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix
338
409
  }
339
410
  return selected;
340
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
+ }
341
618
  function readProjectDispatchPolicy(provider, content) {
342
619
  const frontmatter = getFrontmatterBlock(content);
343
620
  if (!frontmatter) {
@@ -613,7 +890,13 @@ function readResolvedConfigCeiling(provider, resolvedConfig) {
613
890
  return stripConfigCandidateSource(winner);
614
891
  }
615
892
  function normalizeRole(value) {
616
- 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.`);
617
900
  }
618
901
  function providerValueOrder(provider) {
619
902
  if (provider === 'codex') {
@@ -642,6 +925,55 @@ function normalizePreferredValue(provider, value) {
642
925
  }
643
926
  return normalized;
644
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
+ }
645
977
  function normalizeEscalationLevel(value) {
646
978
  if (value === undefined) {
647
979
  return 0;
@@ -664,6 +996,10 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
664
996
  policy: policy.policy,
665
997
  cellSource: policy.cellSource,
666
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,
667
1003
  };
668
1004
  if (policy.mode === 'inherit') {
669
1005
  return {
@@ -677,17 +1013,40 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
677
1013
  target: null,
678
1014
  };
679
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
+ }
680
1031
  if (policy.policy === 'uncapped') {
681
- const selectedValue = role === 'reviewer' ? null : preferredValue;
1032
+ const targetValue = policy.target
1033
+ ? dispatchValueFromRouteTarget(policy.target)
1034
+ : null;
1035
+ const selectedValue = role === 'reviewer' ? null : (targetValue ?? preferredValue);
1036
+ const target = role === 'reviewer' || !targetValue ? null : policy.target;
682
1037
  return {
683
1038
  ...baseSelection,
684
1039
  preferredValue: role === 'reviewer' ? null : preferredValue,
685
1040
  selectedValue,
686
1041
  capped: false,
687
1042
  selectionMode: role === 'reviewer' ? 'no-review-target' : 'uncapped',
688
- selectionBranch: selectedValue ? 'prompt-persisted' : 'unresolved',
689
- family: selectionFamily(provider, selectedValue, null),
690
- target: null,
1043
+ selectionBranch: target
1044
+ ? policy.selectionBranch
1045
+ : selectedValue
1046
+ ? 'prompt-persisted'
1047
+ : 'unresolved',
1048
+ family: selectionFamily(provider, selectedValue, target),
1049
+ target,
691
1050
  };
692
1051
  }
693
1052
  if (policy.value === null) {
@@ -729,6 +1088,10 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
729
1088
  }
730
1089
  const selectedIndex = Math.min(preferredIndex, ceilingIndex);
731
1090
  const selectedValue = order[selectedIndex];
1091
+ const targetValue = policy.target
1092
+ ? dispatchValueFromRouteTarget(policy.target)
1093
+ : null;
1094
+ const selectedTarget = targetValue === selectedValue ? policy.target : null;
732
1095
  return {
733
1096
  ...baseSelection,
734
1097
  preferredValue,
@@ -736,10 +1099,44 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
736
1099
  capped: preferredIndex > ceilingIndex,
737
1100
  selectionMode: 'capped',
738
1101
  selectionBranch: policy.selectionBranch,
739
- family: selectionFamily(provider, selectedValue, selectedValue === policy.value ? policy.target : null),
740
- target: selectedValue === policy.value ? policy.target : null,
1102
+ family: selectionFamily(provider, selectedValue, selectedTarget),
1103
+ target: selectedTarget,
741
1104
  };
742
1105
  }
1106
+ function hasCodexVariantDispatchArgs(dispatchArgs) {
1107
+ return (dispatchArgs !== null &&
1108
+ 'variant' in dispatchArgs &&
1109
+ typeof dispatchArgs.variant === 'string' &&
1110
+ dispatchArgs.variant.length > 0);
1111
+ }
1112
+ function hasModelDispatchArgs(dispatchArgs) {
1113
+ return (dispatchArgs !== null &&
1114
+ 'model' in dispatchArgs &&
1115
+ typeof dispatchArgs.model === 'string' &&
1116
+ dispatchArgs.model.length > 0);
1117
+ }
1118
+ function modelAxis(selection, dispatchArgs) {
1119
+ if (selection.target?.model && dispatchArgs) {
1120
+ return `selected:${selection.target.model}`;
1121
+ }
1122
+ if (hasModelDispatchArgs(dispatchArgs)) {
1123
+ return `selected:${dispatchArgs.model}`;
1124
+ }
1125
+ if (selection.selectionMode === 'inherit-default') {
1126
+ return 'inherited';
1127
+ }
1128
+ return 'unresolved';
1129
+ }
1130
+ function codexEffortAxis(selection, dispatchArgs) {
1131
+ if (hasCodexVariantDispatchArgs(dispatchArgs) && selection.target?.effort) {
1132
+ return `selected:${selection.target.effort}`;
1133
+ }
1134
+ if (selection.selectionMode === 'inherit-default' ||
1135
+ selection.selectionMode === 'no-review-target') {
1136
+ return 'provider-default';
1137
+ }
1138
+ return 'unresolved';
1139
+ }
743
1140
  /**
744
1141
  * Join a resolved ceiling value with the active provider's adapter to compute
745
1142
  * the enforcement mode, mechanism, dispatch args, and verify-on-upgrade flag.
@@ -753,12 +1150,18 @@ function buildProviderResolution(provider, policy, role, orchestratorTier, prefe
753
1150
  mode: adapter.supportsCeiling ? 'advisory' : 'unsupported',
754
1151
  mechanism: adapter.mechanism,
755
1152
  dispatchArgs: null,
1153
+ modelAxis: 'unresolved',
1154
+ effortAxis: provider === 'codex' ? 'provider-default' : 'not-applicable',
756
1155
  verifyOnDispatch: false,
757
1156
  cellSource: null,
758
1157
  target: null,
759
1158
  selection: {
760
1159
  role,
761
1160
  preferredValue,
1161
+ requestedCandidate: null,
1162
+ candidateTier: null,
1163
+ ceilingTier: null,
1164
+ ceilingTarget: null,
762
1165
  selectedValue: null,
763
1166
  capped: false,
764
1167
  selectionMode: 'unresolved',
@@ -777,6 +1180,7 @@ function buildProviderResolution(provider, policy, role, orchestratorTier, prefe
777
1180
  const dispatchArgs = dispatchValue && !isCrossHarness
778
1181
  ? adapter.compileToDispatchArgs(dispatchValue, role, {
779
1182
  orchestratorTier,
1183
+ target: selection.target,
780
1184
  })
781
1185
  : null;
782
1186
  let mode;
@@ -794,9 +1198,14 @@ function buildProviderResolution(provider, policy, role, orchestratorTier, prefe
794
1198
  mode,
795
1199
  mechanism: adapter.mechanism,
796
1200
  dispatchArgs,
1201
+ modelAxis: modelAxis(selection, dispatchArgs),
1202
+ effortAxis: provider === 'codex'
1203
+ ? codexEffortAxis(selection, dispatchArgs)
1204
+ : 'not-applicable',
797
1205
  verifyOnDispatch: dispatchValue && !isCrossHarness
798
1206
  ? adapter.verifyOnDispatch(dispatchValue, {
799
1207
  orchestratorTier,
1208
+ target: selection.target,
800
1209
  })
801
1210
  : false,
802
1211
  cellSource: policy.cellSource,
@@ -851,9 +1260,11 @@ async function resolveDispatchCeiling(context, dependencies, options) {
851
1260
  const providerDefaultEffort = provider === 'codex'
852
1261
  ? await resolveCodexProviderDefaultEffort(repoRoot, context, dependencies)
853
1262
  : 'not-applicable';
1263
+ const requestedCandidate = normalizeRequestedCandidate(provider, role, options);
854
1264
  const preferredValue = normalizePreferredValue(provider, options.preferred);
1265
+ const ceilingTier = normalizeCeilingTier(options.ceilingTier, role);
855
1266
  const escalationLevel = normalizeEscalationLevel(options.escalationLevel);
856
- const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel);
1267
+ const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue, requestedCandidate, ceilingTier);
857
1268
  for (const warning of resolvedValue?.warnings ?? []) {
858
1269
  context.logger.warn(warning);
859
1270
  }
@@ -862,6 +1273,32 @@ async function resolveDispatchCeiling(context, dependencies, options) {
862
1273
  [provider]: providerResolution,
863
1274
  };
864
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
+ }
865
1302
  return {
866
1303
  status: 'resolved',
867
1304
  provider,
@@ -903,25 +1340,111 @@ async function resolveDispatchCeiling(context, dependencies, options) {
903
1340
  * Never reads the preset label for dispatch — the preset is surfaced as
904
1341
  * provenance only.
905
1342
  */
906
- async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel) {
1343
+ async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue, requestedCandidate, invocationCeilingTier) {
907
1344
  const configCeiling = readResolvedConfigCeiling(provider, resolvedConfig);
908
1345
  const projectCeiling = await resolveProjectStateCeiling(provider, projectPath, dependencies);
909
- const baseCeiling = configCeiling ?? projectCeiling;
1346
+ let baseCeiling = configCeiling ?? projectCeiling;
910
1347
  if (!baseCeiling) {
911
1348
  return null;
912
1349
  }
913
- if (baseCeiling.mode === 'inherit' || baseCeiling.policy === 'uncapped') {
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
+ }
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
+ }
1370
+ return baseCeiling;
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
+ }
1398
+ if (baseCeiling.policy === 'uncapped') {
1399
+ const preferredTier = uncappedPreferredTier(provider, role, preferredValue);
1400
+ if (preferredTier) {
1401
+ const matrixCell = resolveProviderMatrixCell(provider, preferredTier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel);
1402
+ if (matrixCell) {
1403
+ return {
1404
+ ...baseCeiling,
1405
+ cellSource: matrixCell.cellSource,
1406
+ target: matrixCell.target,
1407
+ selectionBranch: matrixCell.selectionBranch,
1408
+ warnings: [...baseCeiling.warnings, ...matrixCell.warnings],
1409
+ };
1410
+ }
1411
+ }
914
1412
  return baseCeiling;
915
1413
  }
916
1414
  const tier = policyTier(baseCeiling.policy);
917
1415
  const matrixCell = resolveProviderMatrixCell(provider, tier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel);
918
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
+ }
919
1441
  return {
920
1442
  ...baseCeiling,
921
1443
  value: matrixCell.value,
922
1444
  cellSource: matrixCell.cellSource,
923
1445
  target: matrixCell.target,
924
1446
  selectionBranch: matrixCell.selectionBranch,
1447
+ warnings: [...baseCeiling.warnings, ...matrixCell.warnings],
925
1448
  };
926
1449
  }
927
1450
  if (baseCeiling.policy !== 'legacy-ceiling' &&
@@ -950,6 +1473,15 @@ function writeHumanResolution(context, resolution) {
950
1473
  if (providerResolution) {
951
1474
  context.logger.info(`Mode: ${providerResolution.mode} (${providerResolution.mechanism})`);
952
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
+ }
953
1485
  if (providerResolution.target) {
954
1486
  const details = [
955
1487
  `harness=${providerResolution.target.harness}`,
@@ -1023,18 +1555,49 @@ async function runDispatchCeilingResolve(context, dependencies, options) {
1023
1555
  process.exitCode = 1;
1024
1556
  }
1025
1557
  }
1558
+ async function runDispatchCeilingChoices(context, options) {
1559
+ const choices = getDispatchPolicyChoices();
1560
+ const format = (options.format ?? 'markdown').trim().toLowerCase();
1561
+ if (context.json || options.json) {
1562
+ context.logger.json({ status: 'ok', choices });
1563
+ process.exitCode = 0;
1564
+ return;
1565
+ }
1566
+ if (format !== 'markdown') {
1567
+ context.logger.error(`Invalid choices format "${options.format}". Valid formats: markdown.`);
1568
+ process.exitCode = 1;
1569
+ return;
1570
+ }
1571
+ context.logger.info(renderDispatchPolicyChoicesMarkdown(choices));
1572
+ process.exitCode = 0;
1573
+ }
1026
1574
  export function createProjectDispatchCeilingCommand(overrides = {}) {
1027
1575
  const dependencies = {
1028
1576
  ...DEFAULT_DEPENDENCIES,
1029
1577
  ...overrides,
1030
1578
  };
1031
1579
  const command = new Command('dispatch-ceiling').description('Resolve OAT project dispatch ceiling metadata');
1580
+ command.addCommand(new Command('choices')
1581
+ .description('Print canonical dispatch policy choices')
1582
+ .option('--format <format>', 'Output format: markdown', 'markdown')
1583
+ .option('--json', 'Output machine-readable JSON')
1584
+ .action(async (options, cmd) => {
1585
+ const globalOptions = readGlobalOptions(cmd);
1586
+ const context = dependencies.buildCommandContext({
1587
+ ...globalOptions,
1588
+ json: globalOptions.json === true || options.json === true,
1589
+ });
1590
+ await runDispatchCeilingChoices(context, options);
1591
+ }));
1032
1592
  command.addCommand(new Command('resolve')
1033
1593
  .description('Resolve dispatch policy for a provider')
1034
1594
  .requiredOption('--provider <provider>', 'Provider name: codex, claude, or cursor are enforced; unregistered providers resolve as unsupported advisory')
1035
- .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']))
1036
1596
  .option('--orchestrator-tier <tier>', 'Orchestrator tier, used to flag verify-on-upgrade for above-orchestrator requests')
1037
- .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')
1038
1601
  .option('--escalation-level <level>', 'Ordered route entry to select; 0 is the floor and higher values advance through escalation targets')
1039
1602
  .option('--project-path <path>', 'Read project-state policy from an explicit project path')
1040
1603
  .option('--preflight', 'Treat unresolved non-interactive resolution as an implementation block')