@open-agent-toolkit/cli 0.1.42 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/assets/agents/oat-phase-implementer.md +11 -7
  2. package/assets/agents/oat-reviewer.md +6 -4
  3. package/assets/config/dispatch-matrix-recommendation.json +23 -0
  4. package/assets/docs/cli-utilities/configuration.md +95 -40
  5. package/assets/docs/cli-utilities/workflow-gates.md +79 -16
  6. package/assets/docs/contributing/code.md +10 -4
  7. package/assets/docs/contributing/hooks-and-safety.md +23 -0
  8. package/assets/docs/reference/oat-directory-structure.md +26 -24
  9. package/assets/docs/workflows/projects/dispatch-ceiling.md +193 -80
  10. package/assets/docs/workflows/projects/implementation-execution.md +68 -45
  11. package/assets/docs/workflows/projects/index.md +2 -2
  12. package/assets/docs/workflows/projects/lifecycle.md +17 -2
  13. package/assets/public-package-versions.json +4 -4
  14. package/assets/skills/oat-project-implement/SKILL.md +232 -100
  15. package/assets/skills/oat-project-plan/SKILL.md +80 -30
  16. package/assets/skills/oat-project-plan-writing/SKILL.md +10 -10
  17. package/assets/skills/oat-project-quick-start/SKILL.md +80 -30
  18. package/assets/templates/plan.md +4 -4
  19. package/assets/templates/state.md +12 -3
  20. package/dist/commands/config/index.d.ts +6 -0
  21. package/dist/commands/config/index.d.ts.map +1 -1
  22. package/dist/commands/config/index.js +432 -11
  23. package/dist/commands/doctor/index.d.ts +6 -1
  24. package/dist/commands/doctor/index.d.ts.map +1 -1
  25. package/dist/commands/doctor/index.js +138 -4
  26. package/dist/commands/gate/index.d.ts +39 -2
  27. package/dist/commands/gate/index.d.ts.map +1 -1
  28. package/dist/commands/gate/index.js +343 -30
  29. package/dist/commands/internal/cursor-current-target.d.ts +37 -0
  30. package/dist/commands/internal/cursor-current-target.d.ts.map +1 -0
  31. package/dist/commands/internal/cursor-current-target.js +228 -0
  32. package/dist/commands/internal/index.d.ts.map +1 -1
  33. package/dist/commands/internal/index.js +2 -0
  34. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  35. package/dist/commands/project/dispatch-ceiling/index.js +646 -56
  36. package/dist/config/dispatch-ceiling-preset.d.ts +37 -1
  37. package/dist/config/dispatch-ceiling-preset.d.ts.map +1 -1
  38. package/dist/config/dispatch-ceiling-preset.js +20 -0
  39. package/dist/config/oat-config.d.ts +25 -6
  40. package/dist/config/oat-config.d.ts.map +1 -1
  41. package/dist/config/oat-config.js +123 -11
  42. package/dist/config/resolve.d.ts.map +1 -1
  43. package/dist/config/resolve.js +16 -0
  44. package/dist/manifest/manifest.types.d.ts +12 -12
  45. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  46. package/dist/providers/ceiling/registry.js +22 -1
  47. package/dist/providers/codex/codec/sync-extension.js +1 -1
  48. package/dist/providers/identity/availability.d.ts +22 -0
  49. package/dist/providers/identity/availability.d.ts.map +1 -0
  50. package/dist/providers/identity/availability.js +119 -0
  51. package/dist/providers/identity/family.d.ts +13 -0
  52. package/dist/providers/identity/family.d.ts.map +1 -0
  53. package/dist/providers/identity/family.js +32 -0
  54. package/dist/providers/identity/provenance.d.ts +21 -0
  55. package/dist/providers/identity/provenance.d.ts.map +1 -0
  56. package/dist/providers/identity/provenance.js +65 -0
  57. package/dist/providers/identity/stamp.d.ts +35 -0
  58. package/dist/providers/identity/stamp.d.ts.map +1 -0
  59. package/dist/providers/identity/stamp.js +171 -0
  60. package/package.json +2 -2
@@ -3,23 +3,20 @@ 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_DISPATCH_MATRIX_TIERS, 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';
10
11
  import { getCeilingAdapter, } from '../../../providers/ceiling/registry.js';
12
+ import { classifyModelFamily, } from '../../../providers/identity/family.js';
11
13
  import { Command } from 'commander';
12
14
  import YAML from 'yaml';
13
15
  const CODEX_VALUES = [
14
- 'low',
15
- 'medium',
16
- 'high',
17
- 'xhigh',
16
+ ...VALID_CODEX_DISPATCH_CEILINGS,
18
17
  ];
19
18
  const CLAUDE_VALUES = [
20
- 'haiku',
21
- 'sonnet',
22
- 'opus',
19
+ ...VALID_CLAUDE_DISPATCH_CEILINGS,
23
20
  ];
24
21
  const DEFAULT_DEPENDENCIES = {
25
22
  buildCommandContext,
@@ -58,7 +55,7 @@ function isValidProviderValue(provider, value) {
58
55
  return (typeof value === 'string' &&
59
56
  CLAUDE_VALUES.includes(value));
60
57
  }
61
- return false;
58
+ return typeof value === 'string' && value.trim().length > 0;
62
59
  }
63
60
  function configSourceToCeilingSource(source) {
64
61
  if (source === 'local') {
@@ -104,7 +101,244 @@ function providerLabel(provider) {
104
101
  function resolveTargetProjectPath(repoRoot, projectPath) {
105
102
  return isAbsolute(projectPath) ? projectPath : join(repoRoot, projectPath);
106
103
  }
107
- function readProjectDispatchCeiling(provider, content) {
104
+ function isValidManagedPolicy(value) {
105
+ return (typeof value === 'string' &&
106
+ VALID_MANAGED_DISPATCH_POLICIES.includes(value));
107
+ }
108
+ function validManagedPolicyList() {
109
+ return VALID_MANAGED_DISPATCH_POLICIES.join(', ');
110
+ }
111
+ function validProviderValueList(provider) {
112
+ return providerValueOrder(provider)?.join(', ') ?? 'none';
113
+ }
114
+ function compiledPolicyValueForProvider(provider, compiled) {
115
+ if (!('providers' in compiled)) {
116
+ return null;
117
+ }
118
+ const value = compiled.providers[provider];
119
+ return isValidProviderValue(provider, value) ? value : null;
120
+ }
121
+ function invalidProjectPolicyMessage(value) {
122
+ const actual = typeof value === 'string' ? value : String(value);
123
+ return `Invalid project dispatch policy "${actual}". Valid managed policies: ${validManagedPolicyList()}. Use mode "inherit" for host defaults.`;
124
+ }
125
+ function normalizeProjectMatrixBareValue(provider, value) {
126
+ if (typeof value !== 'string') {
127
+ return undefined;
128
+ }
129
+ const trimmed = value.trim();
130
+ if (!trimmed) {
131
+ return undefined;
132
+ }
133
+ const order = providerValueOrder(provider);
134
+ if (order && !isValidProviderValue(provider, trimmed)) {
135
+ return undefined;
136
+ }
137
+ return trimmed;
138
+ }
139
+ function normalizeProjectMatrixRouteTarget(value) {
140
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
141
+ return undefined;
142
+ }
143
+ const record = value;
144
+ const target = {};
145
+ for (const key of ['harness', 'model', 'effort']) {
146
+ const rawValue = record[key];
147
+ if (typeof rawValue === 'string' && rawValue.trim()) {
148
+ target[key] = rawValue.trim();
149
+ }
150
+ }
151
+ return Object.keys(target).length > 0 ? target : undefined;
152
+ }
153
+ function normalizeProjectMatrixCell(provider, value) {
154
+ const bareValue = normalizeProjectMatrixBareValue(provider, value);
155
+ if (bareValue !== undefined) {
156
+ return bareValue;
157
+ }
158
+ if (!Array.isArray(value) || value.length === 0) {
159
+ return undefined;
160
+ }
161
+ const route = [];
162
+ for (const entry of value) {
163
+ const bareEntry = normalizeProjectMatrixBareValue(provider, entry);
164
+ if (bareEntry !== undefined) {
165
+ route.push(bareEntry);
166
+ continue;
167
+ }
168
+ const target = normalizeProjectMatrixRouteTarget(entry);
169
+ if (target !== undefined) {
170
+ route.push(target);
171
+ }
172
+ }
173
+ return route.length > 0 ? route : undefined;
174
+ }
175
+ function normalizeProjectMatrixProviderValue(provider, value) {
176
+ const bareValue = normalizeProjectMatrixBareValue(provider, value);
177
+ if (bareValue !== undefined) {
178
+ return bareValue;
179
+ }
180
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
181
+ return undefined;
182
+ }
183
+ const tierMap = {};
184
+ for (const [tier, rawCell] of Object.entries(value)) {
185
+ if (!VALID_DISPATCH_MATRIX_TIERS.includes(tier)) {
186
+ continue;
187
+ }
188
+ const normalized = normalizeProjectMatrixCell(provider, rawCell);
189
+ if (normalized !== undefined) {
190
+ tierMap[tier] = normalized;
191
+ }
192
+ }
193
+ return Object.keys(tierMap).length > 0 ? tierMap : undefined;
194
+ }
195
+ function readProjectDispatchMatrix(value) {
196
+ if (value === undefined || value === null) {
197
+ return { matrix: null, warnings: [] };
198
+ }
199
+ if (typeof value !== 'object' || Array.isArray(value)) {
200
+ return {
201
+ matrix: null,
202
+ warnings: [
203
+ 'Ignoring malformed oat_dispatch_policy.matrix in project state.',
204
+ ],
205
+ };
206
+ }
207
+ const matrix = {};
208
+ for (const [provider, rawProviderValue] of Object.entries(value)) {
209
+ if (!provider.trim()) {
210
+ continue;
211
+ }
212
+ const normalized = normalizeProjectMatrixProviderValue(provider, rawProviderValue);
213
+ if (normalized !== undefined) {
214
+ matrix[provider] = normalized;
215
+ }
216
+ }
217
+ if (Object.keys(matrix).length === 0) {
218
+ return {
219
+ matrix: null,
220
+ warnings: [
221
+ 'Ignoring malformed oat_dispatch_policy.matrix in project state.',
222
+ ],
223
+ };
224
+ }
225
+ return { matrix, warnings: [] };
226
+ }
227
+ function policyTier(policy) {
228
+ return typeof policy === 'string' &&
229
+ VALID_DISPATCH_MATRIX_TIERS.includes(policy)
230
+ ? policy
231
+ : null;
232
+ }
233
+ function dispatchValueFromRouteTarget(target) {
234
+ const adapter = getCeilingAdapter(target.harness);
235
+ if (adapter.mechanism === 'pinned-variant') {
236
+ return target.effort ?? null;
237
+ }
238
+ if (adapter.mechanism === 'model-arg') {
239
+ return target.model ?? null;
240
+ }
241
+ return null;
242
+ }
243
+ function routeTargetFromBareValue(provider, value, routeIndex, routeLength) {
244
+ const adapter = getCeilingAdapter(provider);
245
+ const target = {
246
+ harness: provider,
247
+ crossHarness: false,
248
+ routeIndex,
249
+ routeLength,
250
+ };
251
+ if (adapter.mechanism === 'pinned-variant') {
252
+ target.effort = value;
253
+ }
254
+ else {
255
+ target.model = value;
256
+ }
257
+ return target;
258
+ }
259
+ function routeTargetFromObject(provider, target, routeIndex, routeLength) {
260
+ const harness = target.harness ?? provider;
261
+ return {
262
+ harness,
263
+ ...(target.model ? { model: target.model } : {}),
264
+ ...(target.effort ? { effort: target.effort } : {}),
265
+ crossHarness: harness !== provider,
266
+ routeIndex,
267
+ routeLength,
268
+ };
269
+ }
270
+ function resolveRouteMatrixCell(provider, route, escalationLevel, cellSource) {
271
+ const routeIndex = Math.min(escalationLevel, route.length - 1);
272
+ const entry = route[routeIndex];
273
+ if (entry === undefined) {
274
+ return null;
275
+ }
276
+ const target = typeof entry === 'string'
277
+ ? routeTargetFromBareValue(provider, entry, routeIndex, route.length)
278
+ : routeTargetFromObject(provider, entry, routeIndex, route.length);
279
+ const value = typeof entry === 'string' ? entry : dispatchValueFromRouteTarget(target);
280
+ return {
281
+ value,
282
+ cellSource,
283
+ target,
284
+ selectionBranch: routeIndex > 0 ? 'escalation-target' : 'matrix-pinned',
285
+ };
286
+ }
287
+ function resolveProviderCellFromValue(provider, providerValue, tier, cellSource, escalationLevel) {
288
+ if (providerValue === undefined) {
289
+ return null;
290
+ }
291
+ if (typeof providerValue === 'string') {
292
+ return {
293
+ value: providerValue,
294
+ cellSource,
295
+ target: null,
296
+ selectionBranch: 'prompt-persisted',
297
+ };
298
+ }
299
+ if (tier === null) {
300
+ return null;
301
+ }
302
+ const cell = providerValue[tier];
303
+ if (cell === undefined) {
304
+ return null;
305
+ }
306
+ if (typeof cell === 'string') {
307
+ return {
308
+ value: cell,
309
+ cellSource,
310
+ target: null,
311
+ selectionBranch: 'matrix-pinned',
312
+ };
313
+ }
314
+ return resolveRouteMatrixCell(provider, cell, escalationLevel, cellSource);
315
+ }
316
+ function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix, escalationLevel) {
317
+ let selected = null;
318
+ const layers = [
319
+ {
320
+ source: 'user-config',
321
+ providers: resolvedConfig.user.workflow?.dispatchCeiling?.providers,
322
+ },
323
+ {
324
+ source: 'repo-config',
325
+ providers: resolvedConfig.shared.workflow?.dispatchCeiling?.providers,
326
+ },
327
+ {
328
+ source: 'local-config',
329
+ providers: resolvedConfig.local.workflow?.dispatchCeiling?.providers,
330
+ },
331
+ { source: 'project-state', providers: projectMatrix },
332
+ ];
333
+ for (const layer of layers) {
334
+ const resolved = resolveProviderCellFromValue(provider, layer.providers?.[provider], tier, layer.source, escalationLevel);
335
+ if (resolved) {
336
+ selected = resolved;
337
+ }
338
+ }
339
+ return selected;
340
+ }
341
+ function readProjectDispatchPolicy(provider, content) {
108
342
  const frontmatter = getFrontmatterBlock(content);
109
343
  if (!frontmatter) {
110
344
  return null;
@@ -113,6 +347,78 @@ function readProjectDispatchCeiling(provider, content) {
113
347
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
114
348
  return null;
115
349
  }
350
+ const policy = parsed['oat_dispatch_policy'];
351
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
352
+ return null;
353
+ }
354
+ const policyRecord = policy;
355
+ const parsedMatrix = readProjectDispatchMatrix(policyRecord['matrix']);
356
+ const mode = policyRecord['mode'];
357
+ if (mode === 'inherit') {
358
+ return {
359
+ mode,
360
+ policy: null,
361
+ value: null,
362
+ source: 'project-state',
363
+ preset: null,
364
+ matrix: parsedMatrix.matrix,
365
+ cellSource: null,
366
+ target: null,
367
+ selectionBranch: 'inherit',
368
+ warnings: parsedMatrix.warnings,
369
+ };
370
+ }
371
+ if (mode !== 'managed') {
372
+ const actual = typeof mode === 'string' ? mode : String(mode);
373
+ throw new Error(`Invalid project dispatch policy mode "${actual}". Valid modes: managed, inherit.`);
374
+ }
375
+ const policyValue = policyRecord['policy'];
376
+ if (!isValidManagedPolicy(policyValue)) {
377
+ throw new Error(invalidProjectPolicyMessage(policyValue));
378
+ }
379
+ const compiled = compileDispatchPolicyPreset(policyValue);
380
+ if (policyValue === 'uncapped') {
381
+ return {
382
+ mode,
383
+ policy: policyValue,
384
+ value: null,
385
+ source: 'project-state',
386
+ preset: policyValue,
387
+ matrix: parsedMatrix.matrix,
388
+ cellSource: null,
389
+ target: null,
390
+ selectionBranch: 'prompt-persisted',
391
+ warnings: parsedMatrix.warnings,
392
+ };
393
+ }
394
+ const providers = policyRecord['providers'];
395
+ const explicitValue = providers && typeof providers === 'object' && !Array.isArray(providers)
396
+ ? providers[provider]
397
+ : null;
398
+ const providerOrder = providerValueOrder(provider);
399
+ if (providerOrder &&
400
+ explicitValue !== null &&
401
+ explicitValue !== undefined &&
402
+ !isValidProviderValue(provider, explicitValue)) {
403
+ throw new Error(`Invalid project dispatch policy provider value "${String(explicitValue)}" for ${provider}. Valid values: ${validProviderValueList(provider)}.`);
404
+ }
405
+ const value = isValidProviderValue(provider, explicitValue)
406
+ ? explicitValue
407
+ : compiledPolicyValueForProvider(provider, compiled);
408
+ return {
409
+ mode,
410
+ policy: policyValue,
411
+ value,
412
+ source: 'project-state',
413
+ preset: policyValue,
414
+ matrix: parsedMatrix.matrix,
415
+ cellSource: 'project-state',
416
+ target: null,
417
+ selectionBranch: 'prompt-persisted',
418
+ warnings: parsedMatrix.warnings,
419
+ };
420
+ }
421
+ function readLegacyProjectDispatchCeiling(provider, parsed) {
116
422
  const ceiling = parsed['oat_dispatch_ceiling'];
117
423
  if (!ceiling || typeof ceiling !== 'object' || Array.isArray(ceiling)) {
118
424
  return null;
@@ -130,21 +436,42 @@ function readProjectDispatchCeiling(provider, content) {
130
436
  }
131
437
  const presetValue = ceilingRecord['preset'];
132
438
  return {
439
+ mode: 'managed',
440
+ policy: 'legacy-ceiling',
133
441
  value,
442
+ source: 'project-state',
134
443
  preset: typeof presetValue === 'string' ? presetValue : null,
444
+ matrix: null,
445
+ cellSource: 'project-state',
446
+ target: null,
447
+ selectionBranch: 'prompt-persisted',
448
+ warnings: [],
135
449
  };
136
450
  }
451
+ function readProjectDispatchCeiling(provider, content) {
452
+ const frontmatter = getFrontmatterBlock(content);
453
+ if (!frontmatter) {
454
+ return null;
455
+ }
456
+ const parsed = YAML.parse(frontmatter);
457
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
458
+ return null;
459
+ }
460
+ return (readProjectDispatchPolicy(provider, content) ??
461
+ readLegacyProjectDispatchCeiling(provider, parsed));
462
+ }
137
463
  async function resolveProjectStateCeiling(provider, projectPath, dependencies) {
138
464
  if (!projectPath) {
139
465
  return null;
140
466
  }
467
+ let content;
141
468
  try {
142
- const content = await dependencies.readFile(join(projectPath, 'state.md'));
143
- return readProjectDispatchCeiling(provider, content);
469
+ content = await dependencies.readFile(join(projectPath, 'state.md'));
144
470
  }
145
471
  catch {
146
472
  return null;
147
473
  }
474
+ return readProjectDispatchCeiling(provider, content);
148
475
  }
149
476
  async function resolveProjectPath(repoRoot, dependencies, options) {
150
477
  if (options.projectPath) {
@@ -156,20 +483,135 @@ async function resolveProjectPath(repoRoot, dependencies, options) {
156
483
  }
157
484
  return join(repoRoot, activeProject.path);
158
485
  }
159
- function readResolvedConfigCeiling(provider, resolvedConfig) {
486
+ function isConfigCandidateSource(source) {
487
+ return source !== undefined && source !== 'default';
488
+ }
489
+ function configSourcePrecedence(source) {
490
+ switch (source) {
491
+ case 'env':
492
+ return 4;
493
+ case 'local':
494
+ return 3;
495
+ case 'shared':
496
+ return 2;
497
+ case 'user':
498
+ return 1;
499
+ case 'default':
500
+ return 0;
501
+ }
502
+ }
503
+ function lowerPrecedenceConfigSource(left, right) {
504
+ return configSourcePrecedence(left) <= configSourcePrecedence(right)
505
+ ? left
506
+ : right;
507
+ }
508
+ function stripConfigCandidateSource(candidate) {
509
+ return {
510
+ mode: candidate.mode,
511
+ policy: candidate.policy,
512
+ value: candidate.value,
513
+ source: candidate.source,
514
+ preset: candidate.preset,
515
+ matrix: candidate.matrix,
516
+ cellSource: candidate.cellSource,
517
+ target: candidate.target,
518
+ selectionBranch: candidate.selectionBranch,
519
+ warnings: candidate.warnings,
520
+ };
521
+ }
522
+ function readResolvedConfigPolicyCandidate(provider, resolvedConfig) {
523
+ const modeEntry = resolvedConfig.resolved['workflow.dispatchPolicy.mode'];
524
+ const policyEntry = resolvedConfig.resolved['workflow.dispatchPolicy.policy'];
525
+ if (modeEntry?.value === 'inherit' &&
526
+ isConfigCandidateSource(modeEntry.source)) {
527
+ const source = configSourceToCeilingSource(modeEntry.source);
528
+ if (source === null) {
529
+ return null;
530
+ }
531
+ return {
532
+ mode: 'inherit',
533
+ policy: null,
534
+ value: null,
535
+ source,
536
+ preset: null,
537
+ matrix: null,
538
+ cellSource: null,
539
+ target: null,
540
+ selectionBranch: 'inherit',
541
+ warnings: [],
542
+ configSource: modeEntry.source,
543
+ };
544
+ }
545
+ if (modeEntry?.value === 'managed' &&
546
+ isValidManagedPolicy(policyEntry?.value) &&
547
+ isConfigCandidateSource(modeEntry.source) &&
548
+ isConfigCandidateSource(policyEntry?.source)) {
549
+ const policy = policyEntry.value;
550
+ const compiled = compileDispatchPolicyPreset(policy);
551
+ const configSource = lowerPrecedenceConfigSource(modeEntry.source, policyEntry.source);
552
+ const source = configSourceToCeilingSource(configSource);
553
+ if (source === null) {
554
+ return null;
555
+ }
556
+ return {
557
+ mode: 'managed',
558
+ policy,
559
+ value: compiledPolicyValueForProvider(provider, compiled),
560
+ source,
561
+ preset: policy,
562
+ matrix: null,
563
+ cellSource: null,
564
+ target: null,
565
+ selectionBranch: 'prompt-persisted',
566
+ warnings: [],
567
+ configSource,
568
+ };
569
+ }
570
+ return null;
571
+ }
572
+ function readResolvedLegacyConfigCeilingCandidate(provider, resolvedConfig) {
160
573
  // Read the concrete per-provider value from the nested key. The flat
161
574
  // `workflow.dispatchCeiling.<provider>` shape was removed in p01; never read
162
575
  // the preset label for dispatch.
163
576
  const entry = resolvedConfig.resolved[`workflow.dispatchCeiling.providers.${provider}`];
164
- const source = entry ? configSourceToCeilingSource(entry.source) : null;
165
- if (!entry || !source || !isValidProviderValue(provider, entry.value)) {
577
+ if (!entry ||
578
+ !isConfigCandidateSource(entry.source) ||
579
+ !isValidProviderValue(provider, entry.value)) {
580
+ return null;
581
+ }
582
+ const source = configSourceToCeilingSource(entry.source);
583
+ if (source === null) {
166
584
  return null;
167
585
  }
168
586
  return {
587
+ mode: 'managed',
588
+ policy: 'legacy-ceiling',
169
589
  value: entry.value,
170
590
  source,
591
+ preset: null,
592
+ matrix: null,
593
+ cellSource: source,
594
+ target: null,
595
+ selectionBranch: 'prompt-persisted',
596
+ warnings: [],
597
+ configSource: entry.source,
171
598
  };
172
599
  }
600
+ function readResolvedConfigCeiling(provider, resolvedConfig) {
601
+ const policyCandidate = readResolvedConfigPolicyCandidate(provider, resolvedConfig);
602
+ const legacyCandidate = readResolvedLegacyConfigCeilingCandidate(provider, resolvedConfig);
603
+ if (!policyCandidate) {
604
+ return legacyCandidate ? stripConfigCandidateSource(legacyCandidate) : null;
605
+ }
606
+ if (!legacyCandidate) {
607
+ return stripConfigCandidateSource(policyCandidate);
608
+ }
609
+ const winner = configSourcePrecedence(policyCandidate.configSource) >=
610
+ configSourcePrecedence(legacyCandidate.configSource)
611
+ ? policyCandidate
612
+ : legacyCandidate;
613
+ return stripConfigCandidateSource(winner);
614
+ }
173
615
  function normalizeRole(value) {
174
616
  return value === 'reviewer' ? 'reviewer' : 'implementer';
175
617
  }
@@ -200,32 +642,102 @@ function normalizePreferredValue(provider, value) {
200
642
  }
201
643
  return normalized;
202
644
  }
203
- function selectDispatchValue(provider, role, ceilingValue, preferredValue) {
645
+ function normalizeEscalationLevel(value) {
646
+ if (value === undefined) {
647
+ return 0;
648
+ }
649
+ const normalized = value.trim();
650
+ if (!/^\d+$/.test(normalized)) {
651
+ throw new Error(`Invalid escalation level "${value}". Use a non-negative integer.`);
652
+ }
653
+ return Number.parseInt(normalized, 10);
654
+ }
655
+ function selectionFamily(provider, value, target) {
656
+ return value
657
+ ? classifyModelFamily({ value, providerId: target?.harness ?? provider })
658
+ : 'unknown';
659
+ }
660
+ function selectDispatchValue(provider, role, policy, preferredValue) {
661
+ const baseSelection = {
662
+ role,
663
+ policyMode: policy.mode,
664
+ policy: policy.policy,
665
+ cellSource: policy.cellSource,
666
+ target: policy.target,
667
+ };
668
+ if (policy.mode === 'inherit') {
669
+ return {
670
+ ...baseSelection,
671
+ preferredValue: null,
672
+ selectedValue: null,
673
+ capped: false,
674
+ selectionMode: 'inherit-default',
675
+ selectionBranch: 'inherit',
676
+ family: 'unknown',
677
+ target: null,
678
+ };
679
+ }
680
+ if (policy.policy === 'uncapped') {
681
+ const selectedValue = role === 'reviewer' ? null : preferredValue;
682
+ return {
683
+ ...baseSelection,
684
+ preferredValue: role === 'reviewer' ? null : preferredValue,
685
+ selectedValue,
686
+ capped: false,
687
+ selectionMode: role === 'reviewer' ? 'no-review-target' : 'uncapped',
688
+ selectionBranch: selectedValue ? 'prompt-persisted' : 'unresolved',
689
+ family: selectionFamily(provider, selectedValue, null),
690
+ target: null,
691
+ };
692
+ }
693
+ if (policy.value === null) {
694
+ return {
695
+ ...baseSelection,
696
+ preferredValue: null,
697
+ selectedValue: null,
698
+ capped: false,
699
+ selectionMode: policy.target ? 'unresolved' : 'capped',
700
+ selectionBranch: policy.selectionBranch,
701
+ family: 'unknown',
702
+ target: policy.target,
703
+ };
704
+ }
204
705
  if (role === 'reviewer' || preferredValue === null) {
205
706
  return {
206
- role,
707
+ ...baseSelection,
207
708
  preferredValue,
208
- selectedValue: ceilingValue,
709
+ selectedValue: policy.value,
209
710
  capped: false,
711
+ selectionMode: role === 'reviewer' ? 'review-target' : 'capped',
712
+ selectionBranch: policy.selectionBranch,
713
+ family: selectionFamily(provider, policy.value, policy.target),
210
714
  };
211
715
  }
212
716
  const order = providerValueOrder(provider);
213
717
  const preferredIndex = order?.indexOf(preferredValue) ?? -1;
214
- const ceilingIndex = order?.indexOf(ceilingValue) ?? -1;
718
+ const ceilingIndex = order?.indexOf(policy.value) ?? -1;
215
719
  if (!order || preferredIndex < 0 || ceilingIndex < 0) {
216
720
  return {
217
- role,
721
+ ...baseSelection,
218
722
  preferredValue,
219
- selectedValue: ceilingValue,
723
+ selectedValue: policy.value,
220
724
  capped: false,
725
+ selectionMode: 'capped',
726
+ selectionBranch: policy.selectionBranch,
727
+ family: selectionFamily(provider, policy.value, policy.target),
221
728
  };
222
729
  }
223
730
  const selectedIndex = Math.min(preferredIndex, ceilingIndex);
731
+ const selectedValue = order[selectedIndex];
224
732
  return {
225
- role,
733
+ ...baseSelection,
226
734
  preferredValue,
227
- selectedValue: order[selectedIndex],
735
+ selectedValue,
228
736
  capped: preferredIndex > ceilingIndex,
737
+ selectionMode: 'capped',
738
+ selectionBranch: policy.selectionBranch,
739
+ family: selectionFamily(provider, selectedValue, selectedValue === policy.value ? policy.target : null),
740
+ target: selectedValue === policy.value ? policy.target : null,
229
741
  };
230
742
  }
231
743
  /**
@@ -233,28 +745,40 @@ function selectDispatchValue(provider, role, ceilingValue, preferredValue) {
233
745
  * the enforcement mode, mechanism, dispatch args, and verify-on-upgrade flag.
234
746
  * Mode is computed here at call time — it is never read from persisted state.
235
747
  */
236
- function buildProviderResolution(provider, value, role, orchestratorTier, preferredValue) {
748
+ function buildProviderResolution(provider, policy, role, orchestratorTier, preferredValue) {
237
749
  const adapter = getCeilingAdapter(provider);
238
- if (value === null) {
750
+ if (policy === null) {
239
751
  return {
240
752
  value: null,
241
753
  mode: adapter.supportsCeiling ? 'advisory' : 'unsupported',
242
754
  mechanism: adapter.mechanism,
243
755
  dispatchArgs: null,
244
756
  verifyOnDispatch: false,
757
+ cellSource: null,
758
+ target: null,
245
759
  selection: {
246
760
  role,
247
761
  preferredValue,
248
762
  selectedValue: null,
249
763
  capped: false,
764
+ selectionMode: 'unresolved',
765
+ selectionBranch: 'unresolved',
766
+ family: 'unknown',
767
+ target: null,
768
+ cellSource: null,
769
+ policyMode: null,
770
+ policy: null,
250
771
  },
251
772
  };
252
773
  }
253
- const selection = selectDispatchValue(provider, role, value, preferredValue);
254
- const dispatchValue = selection.selectedValue ?? value;
255
- const dispatchArgs = adapter.compileToDispatchArgs(dispatchValue, role, {
256
- orchestratorTier,
257
- });
774
+ const selection = selectDispatchValue(provider, role, policy, preferredValue);
775
+ const dispatchValue = selection.selectedValue;
776
+ const isCrossHarness = selection.target?.crossHarness === true;
777
+ const dispatchArgs = dispatchValue && !isCrossHarness
778
+ ? adapter.compileToDispatchArgs(dispatchValue, role, {
779
+ orchestratorTier,
780
+ })
781
+ : null;
258
782
  let mode;
259
783
  if (!adapter.supportsCeiling) {
260
784
  mode = 'unsupported';
@@ -266,13 +790,17 @@ function buildProviderResolution(provider, value, role, orchestratorTier, prefer
266
790
  mode = 'advisory';
267
791
  }
268
792
  return {
269
- value,
793
+ value: policy.value,
270
794
  mode,
271
795
  mechanism: adapter.mechanism,
272
796
  dispatchArgs,
273
- verifyOnDispatch: adapter.verifyOnDispatch(dispatchValue, {
274
- orchestratorTier,
275
- }),
797
+ verifyOnDispatch: dispatchValue && !isCrossHarness
798
+ ? adapter.verifyOnDispatch(dispatchValue, {
799
+ orchestratorTier,
800
+ })
801
+ : false,
802
+ cellSource: policy.cellSource,
803
+ target: selection.target,
276
804
  selection,
277
805
  };
278
806
  }
@@ -305,7 +833,7 @@ async function resolveCodexProviderDefaultEffort(repoRoot, context, dependencies
305
833
  }
306
834
  function blockMessage(provider) {
307
835
  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.`;
836
+ 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
837
  }
310
838
  function isNonInteractiveEnv(env) {
311
839
  return env['OAT_NON_INTERACTIVE'] === '1';
@@ -324,8 +852,12 @@ async function resolveDispatchCeiling(context, dependencies, options) {
324
852
  ? await resolveCodexProviderDefaultEffort(repoRoot, context, dependencies)
325
853
  : 'not-applicable';
326
854
  const preferredValue = normalizePreferredValue(provider, options.preferred);
327
- const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies);
328
- const providerResolution = buildProviderResolution(provider, resolvedValue?.value ?? null, role, orchestratorTier, preferredValue);
855
+ const escalationLevel = normalizeEscalationLevel(options.escalationLevel);
856
+ const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel);
857
+ for (const warning of resolvedValue?.warnings ?? []) {
858
+ context.logger.warn(warning);
859
+ }
860
+ const providerResolution = buildProviderResolution(provider, resolvedValue, role, orchestratorTier, preferredValue);
329
861
  const providers = {
330
862
  [provider]: providerResolution,
331
863
  };
@@ -334,11 +866,14 @@ async function resolveDispatchCeiling(context, dependencies, options) {
334
866
  status: 'resolved',
335
867
  provider,
336
868
  value: resolvedValue.value,
869
+ policyMode: resolvedValue.mode,
870
+ policy: resolvedValue.policy,
337
871
  source: resolvedValue.source,
338
872
  preset: resolvedValue.preset,
339
873
  unresolved: false,
340
874
  projectPath,
341
875
  providerDefaultEffort,
876
+ matrix: resolvedValue.matrix,
342
877
  providers,
343
878
  };
344
879
  }
@@ -350,11 +885,14 @@ async function resolveDispatchCeiling(context, dependencies, options) {
350
885
  status: shouldBlock ? 'blocked' : 'unresolved',
351
886
  provider,
352
887
  value: null,
888
+ policyMode: null,
889
+ policy: null,
353
890
  source: null,
354
891
  preset: null,
355
892
  unresolved: true,
356
893
  projectPath,
357
894
  providerDefaultEffort,
895
+ matrix: null,
358
896
  providers,
359
897
  message,
360
898
  };
@@ -365,24 +903,39 @@ async function resolveDispatchCeiling(context, dependencies, options) {
365
903
  * Never reads the preset label for dispatch — the preset is surfaced as
366
904
  * provenance only.
367
905
  */
368
- async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies) {
906
+ async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel) {
369
907
  const configCeiling = readResolvedConfigCeiling(provider, resolvedConfig);
370
- if (configCeiling) {
371
- return {
372
- value: configCeiling.value,
373
- source: configCeiling.source,
374
- preset: null,
375
- };
376
- }
377
908
  const projectCeiling = await resolveProjectStateCeiling(provider, projectPath, dependencies);
378
- if (projectCeiling) {
909
+ const baseCeiling = configCeiling ?? projectCeiling;
910
+ if (!baseCeiling) {
911
+ return null;
912
+ }
913
+ if (baseCeiling.mode === 'inherit' || baseCeiling.policy === 'uncapped') {
914
+ return baseCeiling;
915
+ }
916
+ const tier = policyTier(baseCeiling.policy);
917
+ const matrixCell = resolveProviderMatrixCell(provider, tier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel);
918
+ if (matrixCell) {
379
919
  return {
380
- value: projectCeiling.value,
381
- source: 'project-state',
382
- preset: projectCeiling.preset,
920
+ ...baseCeiling,
921
+ value: matrixCell.value,
922
+ cellSource: matrixCell.cellSource,
923
+ target: matrixCell.target,
924
+ selectionBranch: matrixCell.selectionBranch,
383
925
  };
384
926
  }
385
- return null;
927
+ if (baseCeiling.policy !== 'legacy-ceiling' &&
928
+ baseCeiling.value === null &&
929
+ providerValueOrder(provider) === null) {
930
+ return null;
931
+ }
932
+ return baseCeiling;
933
+ }
934
+ function policyLabel(policy) {
935
+ if (policy === 'legacy-ceiling') {
936
+ return 'legacy capped';
937
+ }
938
+ return policy ?? 'inherit host defaults';
386
939
  }
387
940
  function writeHumanResolution(context, resolution) {
388
941
  const label = providerLabel(resolution.provider);
@@ -390,15 +943,48 @@ function writeHumanResolution(context, resolution) {
390
943
  context.logger.error(resolution.message);
391
944
  return;
392
945
  }
393
- context.logger.info(`${label} dispatch ceiling: ${resolution.value ?? 'unresolved'}`);
946
+ context.logger.info(`${label} dispatch policy: ${policyLabel(resolution.policy)}`);
947
+ context.logger.info(`Resolved cap: ${resolution.value ?? 'none'}`);
394
948
  context.logger.info(`Source: ${sourceLabel(resolution.source)}`);
395
949
  const providerResolution = resolution.providers[resolution.provider];
396
950
  if (providerResolution) {
397
951
  context.logger.info(`Mode: ${providerResolution.mode} (${providerResolution.mechanism})`);
952
+ context.logger.info(`Selection: ${providerResolution.selection.selectionMode}`);
953
+ if (providerResolution.target) {
954
+ const details = [
955
+ `harness=${providerResolution.target.harness}`,
956
+ providerResolution.target.model
957
+ ? `model=${providerResolution.target.model}`
958
+ : null,
959
+ providerResolution.target.effort
960
+ ? `effort=${providerResolution.target.effort}`
961
+ : null,
962
+ ]
963
+ .filter(Boolean)
964
+ .join(' ');
965
+ const suffix = providerResolution.target.crossHarness
966
+ ? ' (cross-harness target; native dispatch args deferred)'
967
+ : '';
968
+ context.logger.info(`Dispatch target: ${details}${suffix}`);
969
+ }
398
970
  }
399
971
  if (resolution.provider === 'codex') {
400
972
  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.`);
973
+ if (providerResolution?.selection.selectionMode === 'inherit-default') {
974
+ context.logger.info('Note: OAT will not select a Codex effort; base/unpinned roles resolve through the provider default.');
975
+ }
976
+ else if (providerResolution?.selection.selectionMode === 'uncapped') {
977
+ 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.');
978
+ }
979
+ else if (providerResolution?.selection.selectionMode === 'review-target') {
980
+ context.logger.info('Note: Reviewer dispatch uses the configured target from the resolved capped policy.');
981
+ }
982
+ else if (providerResolution?.selection.selectionMode === 'no-review-target') {
983
+ context.logger.info('Note: Managed uncapped reviewer dispatch has no configured target; use the base/unpinned reviewer fallback.');
984
+ }
985
+ else {
986
+ 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.`);
987
+ }
402
988
  if (providerResolution?.selection.selectedValue &&
403
989
  providerResolution.selection.preferredValue) {
404
990
  context.logger.info(`Selected Codex effort: ${providerResolution.selection.selectedValue}`);
@@ -406,6 +992,9 @@ function writeHumanResolution(context, resolution) {
406
992
  }
407
993
  else {
408
994
  context.logger.info('Effort axis: not-applicable');
995
+ if (providerResolution?.selection.selectionMode === 'inherit-default') {
996
+ context.logger.info('Note: OAT will not select a Claude model; Task dispatch inherits host/provider behavior.');
997
+ }
409
998
  if (providerResolution?.selection.selectedValue &&
410
999
  providerResolution.selection.preferredValue) {
411
1000
  context.logger.info(`Selected dispatch value: ${providerResolution.selection.selectedValue}`);
@@ -441,12 +1030,13 @@ export function createProjectDispatchCeilingCommand(overrides = {}) {
441
1030
  };
442
1031
  const command = new Command('dispatch-ceiling').description('Resolve OAT project dispatch ceiling metadata');
443
1032
  command.addCommand(new Command('resolve')
444
- .description('Resolve dispatch ceiling for a provider')
445
- .requiredOption('--provider <provider>', 'Provider name: codex or claude are enforced; any other provider resolves as advisory (unsupported)')
1033
+ .description('Resolve dispatch policy for a provider')
1034
+ .requiredOption('--provider <provider>', 'Provider name: codex, claude, or cursor are enforced; unregistered providers resolve as unsupported advisory')
446
1035
  .option('--role <role>', 'Dispatch role for variant compilation: implementer (default) or reviewer')
447
1036
  .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')
1037
+ .option('--preferred <value>', 'Preferred implementer/fix dispatch value before applying the resolved policy')
1038
+ .option('--escalation-level <level>', 'Ordered route entry to select; 0 is the floor and higher values advance through escalation targets')
1039
+ .option('--project-path <path>', 'Read project-state policy from an explicit project path')
450
1040
  .option('--preflight', 'Treat unresolved non-interactive resolution as an implementation block')
451
1041
  .option('--non-interactive', 'Force non-interactive block behavior when the ceiling is unresolved')
452
1042
  .option('--json', 'Output machine-readable JSON')