@open-agent-toolkit/cli 0.1.42 → 0.1.43

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 (31) hide show
  1. package/assets/agents/oat-phase-implementer.md +11 -7
  2. package/assets/agents/oat-reviewer.md +6 -4
  3. package/assets/docs/cli-utilities/configuration.md +51 -35
  4. package/assets/docs/reference/oat-directory-structure.md +26 -24
  5. package/assets/docs/workflows/projects/dispatch-ceiling.md +128 -85
  6. package/assets/docs/workflows/projects/implementation-execution.md +68 -45
  7. package/assets/docs/workflows/projects/index.md +2 -2
  8. package/assets/docs/workflows/projects/lifecycle.md +17 -2
  9. package/assets/public-package-versions.json +4 -4
  10. package/assets/skills/oat-project-implement/SKILL.md +181 -91
  11. package/assets/skills/oat-project-plan/SKILL.md +60 -29
  12. package/assets/skills/oat-project-plan-writing/SKILL.md +10 -10
  13. package/assets/skills/oat-project-quick-start/SKILL.md +60 -29
  14. package/assets/templates/plan.md +4 -4
  15. package/assets/templates/state.md +7 -3
  16. package/dist/commands/config/index.d.ts.map +1 -1
  17. package/dist/commands/config/index.js +66 -3
  18. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  19. package/dist/commands/project/dispatch-ceiling/index.js +296 -49
  20. package/dist/config/dispatch-ceiling-preset.d.ts +37 -1
  21. package/dist/config/dispatch-ceiling-preset.d.ts.map +1 -1
  22. package/dist/config/dispatch-ceiling-preset.js +20 -0
  23. package/dist/config/oat-config.d.ts +10 -1
  24. package/dist/config/oat-config.d.ts.map +1 -1
  25. package/dist/config/oat-config.js +20 -1
  26. package/dist/config/resolve.d.ts.map +1 -1
  27. package/dist/config/resolve.js +4 -0
  28. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  29. package/dist/providers/ceiling/registry.js +6 -1
  30. package/dist/providers/codex/codec/sync-extension.js +1 -1
  31. package/package.json +2 -2
@@ -3,7 +3,8 @@ import { isAbsolute, join } from 'node:path';
3
3
  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
- import { resolveActiveProject, } from '../../../config/oat-config.js';
6
+ import { compileDispatchPolicyPreset, } from '../../../config/dispatch-ceiling-preset.js';
7
+ import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_MANAGED_DISPATCH_POLICIES, } from '../../../config/oat-config.js';
7
8
  import { resolveEffectiveConfig, } from '../../../config/resolve.js';
8
9
  import { resolveProjectRoot } from '../../../fs/paths.js';
9
10
  import TOML from '@iarna/toml';
@@ -11,15 +12,10 @@ import { getCeilingAdapter, } from '../../../providers/ceiling/registry.js';
11
12
  import { Command } from 'commander';
12
13
  import YAML from 'yaml';
13
14
  const CODEX_VALUES = [
14
- 'low',
15
- 'medium',
16
- 'high',
17
- 'xhigh',
15
+ ...VALID_CODEX_DISPATCH_CEILINGS,
18
16
  ];
19
17
  const CLAUDE_VALUES = [
20
- 'haiku',
21
- 'sonnet',
22
- 'opus',
18
+ ...VALID_CLAUDE_DISPATCH_CEILINGS,
23
19
  ];
24
20
  const DEFAULT_DEPENDENCIES = {
25
21
  buildCommandContext,
@@ -104,7 +100,28 @@ function providerLabel(provider) {
104
100
  function resolveTargetProjectPath(repoRoot, projectPath) {
105
101
  return isAbsolute(projectPath) ? projectPath : join(repoRoot, projectPath);
106
102
  }
107
- function readProjectDispatchCeiling(provider, content) {
103
+ function isValidManagedPolicy(value) {
104
+ return (typeof value === 'string' &&
105
+ VALID_MANAGED_DISPATCH_POLICIES.includes(value));
106
+ }
107
+ function validManagedPolicyList() {
108
+ return VALID_MANAGED_DISPATCH_POLICIES.join(', ');
109
+ }
110
+ function validProviderValueList(provider) {
111
+ return providerValueOrder(provider)?.join(', ') ?? 'none';
112
+ }
113
+ function compiledPolicyValueForProvider(provider, compiled) {
114
+ if (!('providers' in compiled)) {
115
+ return null;
116
+ }
117
+ const value = compiled.providers[provider];
118
+ return isValidProviderValue(provider, value) ? value : null;
119
+ }
120
+ function invalidProjectPolicyMessage(value) {
121
+ const actual = typeof value === 'string' ? value : String(value);
122
+ return `Invalid project dispatch policy "${actual}". Valid managed policies: ${validManagedPolicyList()}. Use mode "inherit" for host defaults.`;
123
+ }
124
+ function readProjectDispatchPolicy(provider, content) {
108
125
  const frontmatter = getFrontmatterBlock(content);
109
126
  if (!frontmatter) {
110
127
  return null;
@@ -113,6 +130,62 @@ function readProjectDispatchCeiling(provider, content) {
113
130
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
114
131
  return null;
115
132
  }
133
+ const policy = parsed['oat_dispatch_policy'];
134
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
135
+ return null;
136
+ }
137
+ const policyRecord = policy;
138
+ const mode = policyRecord['mode'];
139
+ if (mode === 'inherit') {
140
+ return {
141
+ mode,
142
+ policy: null,
143
+ value: null,
144
+ source: 'project-state',
145
+ preset: null,
146
+ };
147
+ }
148
+ if (mode !== 'managed') {
149
+ const actual = typeof mode === 'string' ? mode : String(mode);
150
+ throw new Error(`Invalid project dispatch policy mode "${actual}". Valid modes: managed, inherit.`);
151
+ }
152
+ const policyValue = policyRecord['policy'];
153
+ if (!isValidManagedPolicy(policyValue)) {
154
+ throw new Error(invalidProjectPolicyMessage(policyValue));
155
+ }
156
+ const compiled = compileDispatchPolicyPreset(policyValue);
157
+ if (policyValue === 'uncapped') {
158
+ return {
159
+ mode,
160
+ policy: policyValue,
161
+ value: null,
162
+ source: 'project-state',
163
+ preset: policyValue,
164
+ };
165
+ }
166
+ const providers = policyRecord['providers'];
167
+ const explicitValue = providers && typeof providers === 'object' && !Array.isArray(providers)
168
+ ? providers[provider]
169
+ : null;
170
+ const providerOrder = providerValueOrder(provider);
171
+ if (providerOrder &&
172
+ explicitValue !== null &&
173
+ explicitValue !== undefined &&
174
+ !isValidProviderValue(provider, explicitValue)) {
175
+ throw new Error(`Invalid project dispatch policy provider value "${String(explicitValue)}" for ${provider}. Valid values: ${validProviderValueList(provider)}.`);
176
+ }
177
+ const value = isValidProviderValue(provider, explicitValue)
178
+ ? explicitValue
179
+ : compiledPolicyValueForProvider(provider, compiled);
180
+ return {
181
+ mode,
182
+ policy: policyValue,
183
+ value,
184
+ source: 'project-state',
185
+ preset: policyValue,
186
+ };
187
+ }
188
+ function readLegacyProjectDispatchCeiling(provider, parsed) {
116
189
  const ceiling = parsed['oat_dispatch_ceiling'];
117
190
  if (!ceiling || typeof ceiling !== 'object' || Array.isArray(ceiling)) {
118
191
  return null;
@@ -130,21 +203,37 @@ function readProjectDispatchCeiling(provider, content) {
130
203
  }
131
204
  const presetValue = ceilingRecord['preset'];
132
205
  return {
206
+ mode: 'managed',
207
+ policy: 'legacy-ceiling',
133
208
  value,
209
+ source: 'project-state',
134
210
  preset: typeof presetValue === 'string' ? presetValue : null,
135
211
  };
136
212
  }
213
+ function readProjectDispatchCeiling(provider, content) {
214
+ const frontmatter = getFrontmatterBlock(content);
215
+ if (!frontmatter) {
216
+ return null;
217
+ }
218
+ const parsed = YAML.parse(frontmatter);
219
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
220
+ return null;
221
+ }
222
+ return (readProjectDispatchPolicy(provider, content) ??
223
+ readLegacyProjectDispatchCeiling(provider, parsed));
224
+ }
137
225
  async function resolveProjectStateCeiling(provider, projectPath, dependencies) {
138
226
  if (!projectPath) {
139
227
  return null;
140
228
  }
229
+ let content;
141
230
  try {
142
- const content = await dependencies.readFile(join(projectPath, 'state.md'));
143
- return readProjectDispatchCeiling(provider, content);
231
+ content = await dependencies.readFile(join(projectPath, 'state.md'));
144
232
  }
145
233
  catch {
146
234
  return null;
147
235
  }
236
+ return readProjectDispatchCeiling(provider, content);
148
237
  }
149
238
  async function resolveProjectPath(repoRoot, dependencies, options) {
150
239
  if (options.projectPath) {
@@ -156,20 +245,115 @@ async function resolveProjectPath(repoRoot, dependencies, options) {
156
245
  }
157
246
  return join(repoRoot, activeProject.path);
158
247
  }
159
- function readResolvedConfigCeiling(provider, resolvedConfig) {
248
+ function isConfigCandidateSource(source) {
249
+ return source !== undefined && source !== 'default';
250
+ }
251
+ function configSourcePrecedence(source) {
252
+ switch (source) {
253
+ case 'env':
254
+ return 4;
255
+ case 'local':
256
+ return 3;
257
+ case 'shared':
258
+ return 2;
259
+ case 'user':
260
+ return 1;
261
+ case 'default':
262
+ return 0;
263
+ }
264
+ }
265
+ function lowerPrecedenceConfigSource(left, right) {
266
+ return configSourcePrecedence(left) <= configSourcePrecedence(right)
267
+ ? left
268
+ : right;
269
+ }
270
+ function stripConfigCandidateSource(candidate) {
271
+ return {
272
+ mode: candidate.mode,
273
+ policy: candidate.policy,
274
+ value: candidate.value,
275
+ source: candidate.source,
276
+ preset: candidate.preset,
277
+ };
278
+ }
279
+ function readResolvedConfigPolicyCandidate(provider, resolvedConfig) {
280
+ const modeEntry = resolvedConfig.resolved['workflow.dispatchPolicy.mode'];
281
+ const policyEntry = resolvedConfig.resolved['workflow.dispatchPolicy.policy'];
282
+ if (modeEntry?.value === 'inherit' &&
283
+ isConfigCandidateSource(modeEntry.source)) {
284
+ const source = configSourceToCeilingSource(modeEntry.source);
285
+ if (source === null) {
286
+ return null;
287
+ }
288
+ return {
289
+ mode: 'inherit',
290
+ policy: null,
291
+ value: null,
292
+ source,
293
+ preset: null,
294
+ configSource: modeEntry.source,
295
+ };
296
+ }
297
+ if (modeEntry?.value === 'managed' &&
298
+ isValidManagedPolicy(policyEntry?.value) &&
299
+ isConfigCandidateSource(modeEntry.source) &&
300
+ isConfigCandidateSource(policyEntry?.source)) {
301
+ const policy = policyEntry.value;
302
+ const compiled = compileDispatchPolicyPreset(policy);
303
+ const configSource = lowerPrecedenceConfigSource(modeEntry.source, policyEntry.source);
304
+ const source = configSourceToCeilingSource(configSource);
305
+ if (source === null) {
306
+ return null;
307
+ }
308
+ return {
309
+ mode: 'managed',
310
+ policy,
311
+ value: compiledPolicyValueForProvider(provider, compiled),
312
+ source,
313
+ preset: policy,
314
+ configSource,
315
+ };
316
+ }
317
+ return null;
318
+ }
319
+ function readResolvedLegacyConfigCeilingCandidate(provider, resolvedConfig) {
160
320
  // Read the concrete per-provider value from the nested key. The flat
161
321
  // `workflow.dispatchCeiling.<provider>` shape was removed in p01; never read
162
322
  // the preset label for dispatch.
163
323
  const entry = resolvedConfig.resolved[`workflow.dispatchCeiling.providers.${provider}`];
164
- const source = entry ? configSourceToCeilingSource(entry.source) : null;
165
- if (!entry || !source || !isValidProviderValue(provider, entry.value)) {
324
+ if (!entry ||
325
+ !isConfigCandidateSource(entry.source) ||
326
+ !isValidProviderValue(provider, entry.value)) {
327
+ return null;
328
+ }
329
+ const source = configSourceToCeilingSource(entry.source);
330
+ if (source === null) {
166
331
  return null;
167
332
  }
168
333
  return {
334
+ mode: 'managed',
335
+ policy: 'legacy-ceiling',
169
336
  value: entry.value,
170
337
  source,
338
+ preset: null,
339
+ configSource: entry.source,
171
340
  };
172
341
  }
342
+ function readResolvedConfigCeiling(provider, resolvedConfig) {
343
+ const policyCandidate = readResolvedConfigPolicyCandidate(provider, resolvedConfig);
344
+ const legacyCandidate = readResolvedLegacyConfigCeilingCandidate(provider, resolvedConfig);
345
+ if (!policyCandidate) {
346
+ return legacyCandidate ? stripConfigCandidateSource(legacyCandidate) : null;
347
+ }
348
+ if (!legacyCandidate) {
349
+ return stripConfigCandidateSource(policyCandidate);
350
+ }
351
+ const winner = configSourcePrecedence(policyCandidate.configSource) >=
352
+ configSourcePrecedence(legacyCandidate.configSource)
353
+ ? policyCandidate
354
+ : legacyCandidate;
355
+ return stripConfigCandidateSource(winner);
356
+ }
173
357
  function normalizeRole(value) {
174
358
  return value === 'reviewer' ? 'reviewer' : 'implementer';
175
359
  }
@@ -200,32 +384,67 @@ function normalizePreferredValue(provider, value) {
200
384
  }
201
385
  return normalized;
202
386
  }
203
- function selectDispatchValue(provider, role, ceilingValue, preferredValue) {
387
+ function selectDispatchValue(provider, role, policy, preferredValue) {
388
+ const baseSelection = {
389
+ role,
390
+ policyMode: policy.mode,
391
+ policy: policy.policy,
392
+ };
393
+ if (policy.mode === 'inherit') {
394
+ return {
395
+ ...baseSelection,
396
+ preferredValue: null,
397
+ selectedValue: null,
398
+ capped: false,
399
+ selectionMode: 'inherit-default',
400
+ };
401
+ }
402
+ if (policy.policy === 'uncapped') {
403
+ return {
404
+ ...baseSelection,
405
+ preferredValue: role === 'reviewer' ? null : preferredValue,
406
+ selectedValue: role === 'reviewer' ? null : preferredValue,
407
+ capped: false,
408
+ selectionMode: role === 'reviewer' ? 'no-review-target' : 'uncapped',
409
+ };
410
+ }
411
+ if (policy.value === null) {
412
+ return {
413
+ ...baseSelection,
414
+ preferredValue: null,
415
+ selectedValue: null,
416
+ capped: false,
417
+ selectionMode: 'capped',
418
+ };
419
+ }
204
420
  if (role === 'reviewer' || preferredValue === null) {
205
421
  return {
206
- role,
422
+ ...baseSelection,
207
423
  preferredValue,
208
- selectedValue: ceilingValue,
424
+ selectedValue: policy.value,
209
425
  capped: false,
426
+ selectionMode: role === 'reviewer' ? 'review-target' : 'capped',
210
427
  };
211
428
  }
212
429
  const order = providerValueOrder(provider);
213
430
  const preferredIndex = order?.indexOf(preferredValue) ?? -1;
214
- const ceilingIndex = order?.indexOf(ceilingValue) ?? -1;
431
+ const ceilingIndex = order?.indexOf(policy.value) ?? -1;
215
432
  if (!order || preferredIndex < 0 || ceilingIndex < 0) {
216
433
  return {
217
- role,
434
+ ...baseSelection,
218
435
  preferredValue,
219
- selectedValue: ceilingValue,
436
+ selectedValue: policy.value,
220
437
  capped: false,
438
+ selectionMode: 'capped',
221
439
  };
222
440
  }
223
441
  const selectedIndex = Math.min(preferredIndex, ceilingIndex);
224
442
  return {
225
- role,
443
+ ...baseSelection,
226
444
  preferredValue,
227
445
  selectedValue: order[selectedIndex],
228
446
  capped: preferredIndex > ceilingIndex,
447
+ selectionMode: 'capped',
229
448
  };
230
449
  }
231
450
  /**
@@ -233,9 +452,9 @@ function selectDispatchValue(provider, role, ceilingValue, preferredValue) {
233
452
  * the enforcement mode, mechanism, dispatch args, and verify-on-upgrade flag.
234
453
  * Mode is computed here at call time — it is never read from persisted state.
235
454
  */
236
- function buildProviderResolution(provider, value, role, orchestratorTier, preferredValue) {
455
+ function buildProviderResolution(provider, policy, role, orchestratorTier, preferredValue) {
237
456
  const adapter = getCeilingAdapter(provider);
238
- if (value === null) {
457
+ if (policy === null) {
239
458
  return {
240
459
  value: null,
241
460
  mode: adapter.supportsCeiling ? 'advisory' : 'unsupported',
@@ -247,14 +466,19 @@ function buildProviderResolution(provider, value, role, orchestratorTier, prefer
247
466
  preferredValue,
248
467
  selectedValue: null,
249
468
  capped: false,
469
+ selectionMode: 'unresolved',
470
+ policyMode: null,
471
+ policy: null,
250
472
  },
251
473
  };
252
474
  }
253
- const selection = selectDispatchValue(provider, role, value, preferredValue);
254
- const dispatchValue = selection.selectedValue ?? value;
255
- const dispatchArgs = adapter.compileToDispatchArgs(dispatchValue, role, {
256
- orchestratorTier,
257
- });
475
+ const selection = selectDispatchValue(provider, role, policy, preferredValue);
476
+ const dispatchValue = selection.selectedValue;
477
+ const dispatchArgs = dispatchValue
478
+ ? adapter.compileToDispatchArgs(dispatchValue, role, {
479
+ orchestratorTier,
480
+ })
481
+ : null;
258
482
  let mode;
259
483
  if (!adapter.supportsCeiling) {
260
484
  mode = 'unsupported';
@@ -266,13 +490,15 @@ function buildProviderResolution(provider, value, role, orchestratorTier, prefer
266
490
  mode = 'advisory';
267
491
  }
268
492
  return {
269
- value,
493
+ value: policy.value,
270
494
  mode,
271
495
  mechanism: adapter.mechanism,
272
496
  dispatchArgs,
273
- verifyOnDispatch: adapter.verifyOnDispatch(dispatchValue, {
274
- orchestratorTier,
275
- }),
497
+ verifyOnDispatch: dispatchValue
498
+ ? adapter.verifyOnDispatch(dispatchValue, {
499
+ orchestratorTier,
500
+ })
501
+ : false,
276
502
  selection,
277
503
  };
278
504
  }
@@ -305,7 +531,7 @@ async function resolveCodexProviderDefaultEffort(repoRoot, context, dependencies
305
531
  }
306
532
  function blockMessage(provider) {
307
533
  const label = providerLabel(provider);
308
- return `BLOCKED: ${label} dispatch ceiling is unresolved in non-interactive mode.\nSet workflow.dispatchCeiling.providers.${provider} in .oat/config.json or oat_dispatch_ceiling in project state.`;
534
+ return `BLOCKED: ${label} dispatch policy is unresolved in non-interactive mode.\nSet workflow.dispatchPolicy.mode/workflow.dispatchPolicy.policy, workflow.dispatchCeiling.providers.${provider}, oat_dispatch_policy, or legacy oat_dispatch_ceiling.`;
309
535
  }
310
536
  function isNonInteractiveEnv(env) {
311
537
  return env['OAT_NON_INTERACTIVE'] === '1';
@@ -325,7 +551,7 @@ async function resolveDispatchCeiling(context, dependencies, options) {
325
551
  : 'not-applicable';
326
552
  const preferredValue = normalizePreferredValue(provider, options.preferred);
327
553
  const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies);
328
- const providerResolution = buildProviderResolution(provider, resolvedValue?.value ?? null, role, orchestratorTier, preferredValue);
554
+ const providerResolution = buildProviderResolution(provider, resolvedValue, role, orchestratorTier, preferredValue);
329
555
  const providers = {
330
556
  [provider]: providerResolution,
331
557
  };
@@ -334,6 +560,8 @@ async function resolveDispatchCeiling(context, dependencies, options) {
334
560
  status: 'resolved',
335
561
  provider,
336
562
  value: resolvedValue.value,
563
+ policyMode: resolvedValue.mode,
564
+ policy: resolvedValue.policy,
337
565
  source: resolvedValue.source,
338
566
  preset: resolvedValue.preset,
339
567
  unresolved: false,
@@ -350,6 +578,8 @@ async function resolveDispatchCeiling(context, dependencies, options) {
350
578
  status: shouldBlock ? 'blocked' : 'unresolved',
351
579
  provider,
352
580
  value: null,
581
+ policyMode: null,
582
+ policy: null,
353
583
  source: null,
354
584
  preset: null,
355
585
  unresolved: true,
@@ -368,37 +598,51 @@ async function resolveDispatchCeiling(context, dependencies, options) {
368
598
  async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies) {
369
599
  const configCeiling = readResolvedConfigCeiling(provider, resolvedConfig);
370
600
  if (configCeiling) {
371
- return {
372
- value: configCeiling.value,
373
- source: configCeiling.source,
374
- preset: null,
375
- };
601
+ return configCeiling;
376
602
  }
377
603
  const projectCeiling = await resolveProjectStateCeiling(provider, projectPath, dependencies);
378
604
  if (projectCeiling) {
379
- return {
380
- value: projectCeiling.value,
381
- source: 'project-state',
382
- preset: projectCeiling.preset,
383
- };
605
+ return projectCeiling;
384
606
  }
385
607
  return null;
386
608
  }
609
+ function policyLabel(policy) {
610
+ if (policy === 'legacy-ceiling') {
611
+ return 'legacy capped';
612
+ }
613
+ return policy ?? 'inherit host defaults';
614
+ }
387
615
  function writeHumanResolution(context, resolution) {
388
616
  const label = providerLabel(resolution.provider);
389
617
  if (resolution.status === 'blocked' && resolution.message) {
390
618
  context.logger.error(resolution.message);
391
619
  return;
392
620
  }
393
- context.logger.info(`${label} dispatch ceiling: ${resolution.value ?? 'unresolved'}`);
621
+ context.logger.info(`${label} dispatch policy: ${policyLabel(resolution.policy)}`);
622
+ context.logger.info(`Resolved cap: ${resolution.value ?? 'none'}`);
394
623
  context.logger.info(`Source: ${sourceLabel(resolution.source)}`);
395
624
  const providerResolution = resolution.providers[resolution.provider];
396
625
  if (providerResolution) {
397
626
  context.logger.info(`Mode: ${providerResolution.mode} (${providerResolution.mechanism})`);
627
+ context.logger.info(`Selection: ${providerResolution.selection.selectionMode}`);
398
628
  }
399
629
  if (resolution.provider === 'codex') {
400
630
  context.logger.info(`Codex provider default effort: ${resolution.providerDefaultEffort}`);
401
- context.logger.info(`Note: OAT will use pinned subagent variants up to ${resolution.value ?? 'the resolved ceiling'}. Base/unpinned roles resolve through the provider default.`);
631
+ if (providerResolution?.selection.selectionMode === 'inherit-default') {
632
+ context.logger.info('Note: OAT will not select a Codex effort; base/unpinned roles resolve through the provider default.');
633
+ }
634
+ else if (providerResolution?.selection.selectionMode === 'uncapped') {
635
+ context.logger.info('Note: OAT will use the preferred pinned Codex variant with no configured cap. Actual host support for upward effort selection must be verified by the dispatching host.');
636
+ }
637
+ else if (providerResolution?.selection.selectionMode === 'review-target') {
638
+ context.logger.info('Note: Reviewer dispatch uses the configured target from the resolved capped policy.');
639
+ }
640
+ else if (providerResolution?.selection.selectionMode === 'no-review-target') {
641
+ context.logger.info('Note: Managed uncapped reviewer dispatch has no configured target; use the base/unpinned reviewer fallback.');
642
+ }
643
+ else {
644
+ context.logger.info(`Note: OAT will use pinned subagent variants up to ${resolution.value ?? 'the resolved policy cap'}. Base/unpinned roles resolve through the provider default.`);
645
+ }
402
646
  if (providerResolution?.selection.selectedValue &&
403
647
  providerResolution.selection.preferredValue) {
404
648
  context.logger.info(`Selected Codex effort: ${providerResolution.selection.selectedValue}`);
@@ -406,6 +650,9 @@ function writeHumanResolution(context, resolution) {
406
650
  }
407
651
  else {
408
652
  context.logger.info('Effort axis: not-applicable');
653
+ if (providerResolution?.selection.selectionMode === 'inherit-default') {
654
+ context.logger.info('Note: OAT will not select a Claude model; Task dispatch inherits host/provider behavior.');
655
+ }
409
656
  if (providerResolution?.selection.selectedValue &&
410
657
  providerResolution.selection.preferredValue) {
411
658
  context.logger.info(`Selected dispatch value: ${providerResolution.selection.selectedValue}`);
@@ -441,12 +688,12 @@ export function createProjectDispatchCeilingCommand(overrides = {}) {
441
688
  };
442
689
  const command = new Command('dispatch-ceiling').description('Resolve OAT project dispatch ceiling metadata');
443
690
  command.addCommand(new Command('resolve')
444
- .description('Resolve dispatch ceiling for a provider')
691
+ .description('Resolve dispatch policy for a provider')
445
692
  .requiredOption('--provider <provider>', 'Provider name: codex or claude are enforced; any other provider resolves as advisory (unsupported)')
446
693
  .option('--role <role>', 'Dispatch role for variant compilation: implementer (default) or reviewer')
447
694
  .option('--orchestrator-tier <tier>', 'Orchestrator tier, used to flag verify-on-upgrade for above-orchestrator requests')
448
- .option('--preferred <value>', 'Preferred implementer/fix dispatch value before capping by the resolved ceiling')
449
- .option('--project-path <path>', 'Read project-state ceiling from an explicit project path')
695
+ .option('--preferred <value>', 'Preferred implementer/fix dispatch value before applying the resolved policy')
696
+ .option('--project-path <path>', 'Read project-state policy from an explicit project path')
450
697
  .option('--preflight', 'Treat unresolved non-interactive resolution as an implementation block')
451
698
  .option('--non-interactive', 'Force non-interactive block behavior when the ceiling is unresolved')
452
699
  .option('--json', 'Output machine-readable JSON')
@@ -1,4 +1,4 @@
1
- import type { WorkflowClaudeDispatchCeiling, WorkflowCodexDispatchCeiling, WorkflowDispatchCeiling, WorkflowDispatchCeilingPreset } from './oat-config.js';
1
+ import type { WorkflowClaudeDispatchCeiling, WorkflowCodexDispatchCeiling, WorkflowDispatchCeiling, WorkflowDispatchCeilingPreset, WorkflowManagedDispatchPolicy } from './oat-config.js';
2
2
  /**
3
3
  * Fixed mapping table: preset → concrete per-provider values.
4
4
  * This is the single authority for preset compilation; the resolver and skills
@@ -22,6 +22,26 @@ export declare const DISPATCH_CEILING_PRESETS: {
22
22
  readonly claude: "sonnet";
23
23
  };
24
24
  };
25
+ export type DispatchPolicyClaudeValue = WorkflowClaudeDispatchCeiling | 'fable';
26
+ export type CappedManagedDispatchPolicy = Exclude<WorkflowManagedDispatchPolicy, 'uncapped'>;
27
+ export declare const DISPATCH_POLICY_PRESETS: {
28
+ readonly economy: {
29
+ readonly codex: "medium";
30
+ readonly claude: "sonnet";
31
+ };
32
+ readonly balanced: {
33
+ readonly codex: "high";
34
+ readonly claude: "sonnet";
35
+ };
36
+ readonly high: {
37
+ readonly codex: "xhigh";
38
+ readonly claude: "opus";
39
+ };
40
+ readonly frontier: {
41
+ readonly codex: "xhigh";
42
+ readonly claude: "fable";
43
+ };
44
+ };
25
45
  /**
26
46
  * Result of compiling a preset selection.
27
47
  * Both `preset` (provenance label) and `providers` (concrete values) are present.
@@ -40,6 +60,17 @@ export interface PresetCompileResult {
40
60
  export interface AdvancedCompileResult {
41
61
  providers: WorkflowDispatchCeiling['providers'] & object;
42
62
  }
63
+ export type DispatchPolicyCompileResult = {
64
+ mode: 'managed';
65
+ policy: CappedManagedDispatchPolicy;
66
+ providers: {
67
+ codex: WorkflowCodexDispatchCeiling;
68
+ claude: DispatchPolicyClaudeValue;
69
+ };
70
+ } | {
71
+ mode: 'managed';
72
+ policy: 'uncapped';
73
+ };
43
74
  /**
44
75
  * Compile a preset label into concrete per-provider values.
45
76
  * The returned `preset` field is provenance only; runtime dispatch reads
@@ -51,4 +82,9 @@ export declare function compileDispatchCeilingPreset(preset: WorkflowDispatchCei
51
82
  * Advanced selections do not set a preset so the persisted shape omits it.
52
83
  */
53
84
  export declare function compileAdvancedDispatchCeiling(providers: WorkflowDispatchCeiling['providers'] & object): AdvancedCompileResult;
85
+ /**
86
+ * Compile the managed dispatch-policy ladder into concrete provider caps.
87
+ * `uncapped` is intentionally explicit but has no provider caps.
88
+ */
89
+ export declare function compileDispatchPolicyPreset(policy: WorkflowManagedDispatchPolicy): DispatchPolicyCompileResult;
54
90
  //# sourceMappingURL=dispatch-ceiling-preset.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch-ceiling-preset.d.ts","sourceRoot":"","sources":["../../src/config/dispatch-ceiling-preset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,6BAA6B,EAC7B,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC9B,MAAM,cAAc,CAAC;AAEtB;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;CAOpC,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,6BAA6B,CAAC;IACtC,SAAS,EAAE;QACT,KAAK,EAAE,4BAA4B,CAAC;QACpC,MAAM,EAAE,6BAA6B,CAAC;KACvC,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,uBAAuB,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC;CAC1D;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,6BAA6B,GACpC,mBAAmB,CAKrB;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,uBAAuB,CAAC,WAAW,CAAC,GAAG,MAAM,GACvD,qBAAqB,CAEvB"}
1
+ {"version":3,"file":"dispatch-ceiling-preset.d.ts","sourceRoot":"","sources":["../../src/config/dispatch-ceiling-preset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,6BAA6B,EAC7B,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAC7B,6BAA6B,EAC9B,MAAM,cAAc,CAAC;AAEtB;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;CAOpC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,6BAA6B,GAAG,OAAO,CAAC;AAChF,MAAM,MAAM,2BAA2B,GAAG,OAAO,CAC/C,6BAA6B,EAC7B,UAAU,CACX,CAAC;AAEF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;CAQnC,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,6BAA6B,CAAC;IACtC,SAAS,EAAE;QACT,KAAK,EAAE,4BAA4B,CAAC;QACpC,MAAM,EAAE,6BAA6B,CAAC;KACvC,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,uBAAuB,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC;CAC1D;AAED,MAAM,MAAM,2BAA2B,GACnC;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,2BAA2B,CAAC;IACpC,SAAS,EAAE;QACT,KAAK,EAAE,4BAA4B,CAAC;QACpC,MAAM,EAAE,yBAAyB,CAAC;KACnC,CAAC;CACH,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEN;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,6BAA6B,GACpC,mBAAmB,CAKrB;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,uBAAuB,CAAC,WAAW,CAAC,GAAG,MAAM,GACvD,qBAAqB,CAEvB;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,6BAA6B,GACpC,2BAA2B,CAU7B"}
@@ -12,6 +12,12 @@ export const DISPATCH_CEILING_PRESETS = {
12
12
  maximum: { codex: 'xhigh', claude: 'opus' },
13
13
  'cost-conscious': { codex: 'medium', claude: 'sonnet' },
14
14
  };
15
+ export const DISPATCH_POLICY_PRESETS = {
16
+ economy: { codex: 'medium', claude: 'sonnet' },
17
+ balanced: { codex: 'high', claude: 'sonnet' },
18
+ high: { codex: 'xhigh', claude: 'opus' },
19
+ frontier: { codex: 'xhigh', claude: 'fable' },
20
+ };
15
21
  /**
16
22
  * Compile a preset label into concrete per-provider values.
17
23
  * The returned `preset` field is provenance only; runtime dispatch reads
@@ -30,3 +36,17 @@ export function compileDispatchCeilingPreset(preset) {
30
36
  export function compileAdvancedDispatchCeiling(providers) {
31
37
  return { providers };
32
38
  }
39
+ /**
40
+ * Compile the managed dispatch-policy ladder into concrete provider caps.
41
+ * `uncapped` is intentionally explicit but has no provider caps.
42
+ */
43
+ export function compileDispatchPolicyPreset(policy) {
44
+ if (policy === 'uncapped') {
45
+ return { mode: 'managed', policy };
46
+ }
47
+ return {
48
+ mode: 'managed',
49
+ policy,
50
+ providers: { ...DISPATCH_POLICY_PRESETS[policy] },
51
+ };
52
+ }
@@ -21,8 +21,10 @@ export type WorkflowPostImplementSequence = 'wait' | 'summary' | 'pr' | 'docs-pr
21
21
  export type WorkflowReviewExecutionModel = 'subagent' | 'inline' | 'fresh-session';
22
22
  export type WorkflowDesignMode = 'collaborative' | 'selective' | 'draft';
23
23
  export type WorkflowCodexDispatchCeiling = 'low' | 'medium' | 'high' | 'xhigh';
24
- export type WorkflowClaudeDispatchCeiling = 'haiku' | 'sonnet' | 'opus';
24
+ export type WorkflowClaudeDispatchCeiling = 'haiku' | 'sonnet' | 'opus' | 'fable';
25
25
  export type WorkflowDispatchCeilingPreset = 'balanced' | 'maximum' | 'cost-conscious';
26
+ export type WorkflowDispatchPolicyMode = 'managed' | 'inherit';
27
+ export type WorkflowManagedDispatchPolicy = 'economy' | 'balanced' | 'high' | 'frontier' | 'uncapped';
26
28
  export type GateOnFailure = 'block' | 'prompt' | 'warn';
27
29
  export type GateAvoid = 'same-runtime' | 'none';
28
30
  export interface WorkflowDispatchCeiling {
@@ -32,6 +34,10 @@ export interface WorkflowDispatchCeiling {
32
34
  claude?: WorkflowClaudeDispatchCeiling;
33
35
  };
34
36
  }
37
+ export interface WorkflowDispatchPolicy {
38
+ mode: WorkflowDispatchPolicyMode;
39
+ policy?: WorkflowManagedDispatchPolicy;
40
+ }
35
41
  export interface WorkflowAutoArtifactReview {
36
42
  plan?: boolean;
37
43
  analysis?: boolean;
@@ -64,12 +70,15 @@ export interface OatWorkflowConfig {
64
70
  autoNarrowReReviewScope?: boolean;
65
71
  autoArtifactReview?: WorkflowAutoArtifactReview;
66
72
  designMode?: WorkflowDesignMode;
73
+ dispatchPolicy?: WorkflowDispatchPolicy;
67
74
  dispatchCeiling?: WorkflowDispatchCeiling;
68
75
  gates?: WorkflowGatesConfig;
69
76
  }
70
77
  export declare const VALID_CODEX_DISPATCH_CEILINGS: readonly WorkflowCodexDispatchCeiling[];
71
78
  export declare const VALID_CLAUDE_DISPATCH_CEILINGS: readonly WorkflowClaudeDispatchCeiling[];
72
79
  export declare const VALID_DISPATCH_CEILING_PRESETS: readonly WorkflowDispatchCeilingPreset[];
80
+ export declare const VALID_DISPATCH_POLICY_MODES: readonly WorkflowDispatchPolicyMode[];
81
+ export declare const VALID_MANAGED_DISPATCH_POLICIES: readonly WorkflowManagedDispatchPolicy[];
73
82
  export declare const BUILTIN_EXEC_TARGETS: Readonly<Record<string, ExecTarget>>;
74
83
  export type OatToolsConfig = Partial<Record<'core' | 'ideas' | 'docs' | 'workflows' | 'utility' | 'project-management' | 'research' | 'brainstorm', boolean>>;
75
84
  export interface OatConfig {