@open-agent-toolkit/cli 0.1.46 → 0.1.50
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.
- package/assets/agents/oat-phase-implementer.md +202 -191
- package/assets/agents/oat-reviewer.md +11 -1
- package/assets/config/dispatch-matrix-recommendation.json +120 -13
- package/assets/docs/cli-utilities/configuration.md +213 -113
- package/assets/docs/cli-utilities/workflow-gates.md +178 -27
- package/assets/docs/contributing/skills.md +14 -8
- package/assets/docs/provider-sync/providers.md +46 -8
- package/assets/docs/provider-sync/scope-and-surface.md +2 -2
- package/assets/docs/reference/cli-reference.md +3 -1
- package/assets/docs/reference/oat-directory-structure.md +27 -26
- package/assets/docs/workflows/projects/artifacts.md +31 -1
- package/assets/docs/workflows/projects/dispatch-ceiling.md +297 -197
- package/assets/docs/workflows/projects/implementation-execution.md +306 -249
- package/assets/docs/workflows/projects/lifecycle.md +26 -5
- package/assets/docs/workflows/projects/reviews.md +27 -2
- package/assets/migration/pjm-restructure.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-project-implement/SKILL.md +290 -149
- package/assets/skills/oat-project-import-plan/SKILL.md +173 -16
- package/assets/skills/oat-project-next/SKILL.md +2 -2
- package/assets/skills/oat-project-plan/SKILL.md +122 -92
- package/assets/skills/oat-project-plan-writing/SKILL.md +246 -15
- package/assets/skills/oat-project-quick-start/SKILL.md +157 -107
- package/assets/skills/oat-project-review-provide/SKILL.md +110 -22
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +28 -1
- package/dist/commands/config/index.d.ts +3 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +92 -125
- package/dist/commands/doctor/index.d.ts +3 -0
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +57 -95
- package/dist/commands/gate/index.d.ts +7 -1
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +615 -67
- package/dist/commands/gate/review-verdict.d.ts +16 -0
- package/dist/commands/gate/review-verdict.d.ts.map +1 -1
- package/dist/commands/gate/review-verdict.js +72 -9
- package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch-ceiling/index.js +567 -99
- package/dist/commands/providers/codex/materialize.d.ts.map +1 -1
- package/dist/commands/providers/codex/materialize.js +6 -1
- package/dist/commands/shared/frontmatter.d.ts +4 -0
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +23 -0
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +10 -4
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +10 -3
- package/dist/commands/sync/sync.types.d.ts +1 -0
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/config/dispatch-matrix.d.ts +59 -0
- package/dist/config/dispatch-matrix.d.ts.map +1 -0
- package/dist/config/dispatch-matrix.js +264 -0
- package/dist/config/oat-config.d.ts +9 -18
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +99 -120
- package/dist/config/resolve.d.ts +8 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +66 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/scanner.d.ts +1 -0
- package/dist/engine/scanner.d.ts.map +1 -1
- package/dist/engine/scanner.js +17 -1
- package/dist/fs/paths.d.ts +4 -0
- package/dist/fs/paths.d.ts.map +1 -1
- package/dist/fs/paths.js +18 -1
- package/dist/providers/ceiling/registry.d.ts +1 -0
- package/dist/providers/ceiling/registry.d.ts.map +1 -1
- package/dist/providers/ceiling/registry.js +12 -5
- package/dist/providers/codex/codec/catalog.d.ts +14 -0
- package/dist/providers/codex/codec/catalog.d.ts.map +1 -0
- package/dist/providers/codex/codec/catalog.js +21 -0
- package/dist/providers/codex/codec/materialize.d.ts.map +1 -1
- package/dist/providers/codex/codec/materialize.js +6 -5
- package/dist/providers/codex/codec/shared.d.ts +19 -0
- package/dist/providers/codex/codec/shared.d.ts.map +1 -1
- package/dist/providers/codex/codec/shared.js +98 -5
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +142 -32
- package/dist/providers/identity/availability.d.ts +12 -0
- package/dist/providers/identity/availability.d.ts.map +1 -1
- package/dist/providers/identity/availability.js +72 -29
- package/dist/providers/identity/dispatch-report.d.ts +124 -0
- package/dist/providers/identity/dispatch-report.d.ts.map +1 -0
- package/dist/providers/identity/dispatch-report.js +285 -0
- package/dist/providers/identity/dispatch-validation.d.ts +28 -0
- package/dist/providers/identity/dispatch-validation.d.ts.map +1 -0
- package/dist/providers/identity/dispatch-validation.js +149 -0
- package/dist/providers/identity/stamp.d.ts +2 -0
- package/dist/providers/identity/stamp.d.ts.map +1 -1
- package/dist/providers/identity/stamp.js +7 -1
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +4 -0
- package/package.json +2 -2
|
@@ -4,14 +4,16 @@ 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 { normalizeDispatchMatrix } from '../../../config/dispatch-matrix.js';
|
|
7
8
|
import { getDispatchPolicyChoices, managedDispatchPolicyValueList, renderDispatchPolicyChoicesMarkdown, } from '../../../config/dispatch-policy-options.js';
|
|
8
|
-
import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_DISPATCH_MATRIX_TIERS, VALID_MANAGED_DISPATCH_POLICIES, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
|
|
9
|
+
import { resolveActiveProject, VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, VALID_DISPATCH_MATRIX_TIERS, VALID_MANAGED_DISPATCH_POLICIES, isWorkflowDispatchCandidateLadder, isWorkflowDispatchFallbackRoute, toWorkflowDispatchCandidateLadder, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
|
|
9
10
|
import { resolveEffectiveConfig, } from '../../../config/resolve.js';
|
|
10
11
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
11
12
|
import TOML from '@iarna/toml';
|
|
12
|
-
import { getCeilingAdapter, } from '../../../providers/ceiling/registry.js';
|
|
13
|
+
import { getCeilingAdapter, isDirectDispatchRoleName, CLAUDE_TIER_ORDER, } from '../../../providers/ceiling/registry.js';
|
|
14
|
+
import { buildDispatchReport, formatDispatchReport, } from '../../../providers/identity/dispatch-report.js';
|
|
13
15
|
import { classifyModelFamily, } from '../../../providers/identity/family.js';
|
|
14
|
-
import { Command } from 'commander';
|
|
16
|
+
import { Command, Option } from 'commander';
|
|
15
17
|
import YAML from 'yaml';
|
|
16
18
|
const CODEX_VALUES = [
|
|
17
19
|
...VALID_CODEX_DISPATCH_CEILINGS,
|
|
@@ -85,6 +87,8 @@ function sourceLabel(source) {
|
|
|
85
87
|
return 'environment';
|
|
86
88
|
case 'project-state':
|
|
87
89
|
return 'project state';
|
|
90
|
+
case 'invocation':
|
|
91
|
+
return 'explicit invocation';
|
|
88
92
|
default:
|
|
89
93
|
return 'none';
|
|
90
94
|
}
|
|
@@ -123,107 +127,64 @@ function invalidProjectPolicyMessage(value) {
|
|
|
123
127
|
const actual = typeof value === 'string' ? value : String(value);
|
|
124
128
|
return `Invalid project dispatch policy "${actual}". Valid managed policies: ${validManagedPolicyList()}. Use mode "inherit" for host defaults.`;
|
|
125
129
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
const trimmed = value.trim();
|
|
131
|
-
if (!trimmed) {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
const order = providerValueOrder(provider);
|
|
135
|
-
if (order && !isValidProviderValue(provider, trimmed)) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
return trimmed;
|
|
139
|
-
}
|
|
140
|
-
function normalizeProjectMatrixRouteTarget(value) {
|
|
141
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
const record = value;
|
|
145
|
-
const target = {};
|
|
146
|
-
for (const key of ['harness', 'model', 'effort']) {
|
|
147
|
-
const rawValue = record[key];
|
|
148
|
-
if (typeof rawValue === 'string' && rawValue.trim()) {
|
|
149
|
-
target[key] = rawValue.trim();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return Object.keys(target).length > 0 ? target : undefined;
|
|
130
|
+
function isProjectMatrixRecord(value) {
|
|
131
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
153
132
|
}
|
|
154
|
-
function
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
const route = [];
|
|
163
|
-
for (const entry of value) {
|
|
164
|
-
const bareEntry = normalizeProjectMatrixBareValue(provider, entry);
|
|
165
|
-
if (bareEntry !== undefined) {
|
|
166
|
-
route.push(bareEntry);
|
|
133
|
+
function toProjectMatrixCompatibility(rawMatrix, matrix) {
|
|
134
|
+
const compatibility = {};
|
|
135
|
+
for (const [provider, providerValue] of Object.entries(matrix)) {
|
|
136
|
+
if (typeof providerValue === 'string') {
|
|
137
|
+
compatibility[provider] = providerValue;
|
|
167
138
|
continue;
|
|
168
139
|
}
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const normalized = normalizeProjectMatrixCell(provider, rawCell);
|
|
190
|
-
if (normalized !== undefined) {
|
|
191
|
-
tierMap[tier] = normalized;
|
|
140
|
+
const rawProvider = isProjectMatrixRecord(rawMatrix[provider])
|
|
141
|
+
? rawMatrix[provider]
|
|
142
|
+
: {};
|
|
143
|
+
const tiers = {};
|
|
144
|
+
for (const [tier, cell] of Object.entries(providerValue)) {
|
|
145
|
+
const matrixTier = tier;
|
|
146
|
+
const rawCell = rawProvider[matrixTier];
|
|
147
|
+
const ladder = toWorkflowDispatchCandidateLadder(cell);
|
|
148
|
+
const firstCandidate = ladder.candidates[0];
|
|
149
|
+
if (typeof rawCell === 'string' && typeof firstCandidate === 'string') {
|
|
150
|
+
tiers[matrixTier] = firstCandidate;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(rawCell) &&
|
|
154
|
+
firstCandidate !== undefined &&
|
|
155
|
+
isWorkflowDispatchFallbackRoute(firstCandidate)) {
|
|
156
|
+
tiers[matrixTier] = firstCandidate.route;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
tiers[matrixTier] = cell;
|
|
192
160
|
}
|
|
161
|
+
compatibility[provider] = tiers;
|
|
193
162
|
}
|
|
194
|
-
return
|
|
163
|
+
return compatibility;
|
|
195
164
|
}
|
|
196
165
|
function readProjectDispatchMatrix(value) {
|
|
197
166
|
if (value === undefined || value === null) {
|
|
198
|
-
return { matrix: null, warnings: [] };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
const matrix = {};
|
|
209
|
-
for (const [provider, rawProviderValue] of Object.entries(value)) {
|
|
210
|
-
if (!provider.trim()) {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
const normalized = normalizeProjectMatrixProviderValue(provider, rawProviderValue);
|
|
214
|
-
if (normalized !== undefined) {
|
|
215
|
-
matrix[provider] = normalized;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (Object.keys(matrix).length === 0) {
|
|
167
|
+
return { matrix: null, compatibilityMatrix: null, warnings: [] };
|
|
168
|
+
}
|
|
169
|
+
const normalized = normalizeDispatchMatrix(value, {
|
|
170
|
+
pathPrefix: 'oat_dispatch_policy.matrix',
|
|
171
|
+
compatibilityMode: 'project-state',
|
|
172
|
+
});
|
|
173
|
+
const matrix = Object.fromEntries(Object.entries(normalized.providers).filter(([provider]) => Boolean(provider.trim())));
|
|
174
|
+
if (!isProjectMatrixRecord(value) || Object.keys(matrix).length === 0) {
|
|
219
175
|
return {
|
|
220
176
|
matrix: null,
|
|
177
|
+
compatibilityMatrix: null,
|
|
221
178
|
warnings: [
|
|
222
179
|
'Ignoring malformed oat_dispatch_policy.matrix in project state.',
|
|
223
180
|
],
|
|
224
181
|
};
|
|
225
182
|
}
|
|
226
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
matrix,
|
|
185
|
+
compatibilityMatrix: toProjectMatrixCompatibility(value, matrix),
|
|
186
|
+
warnings: [],
|
|
187
|
+
};
|
|
227
188
|
}
|
|
228
189
|
function policyTier(policy) {
|
|
229
190
|
return typeof policy === 'string' &&
|
|
@@ -343,6 +304,19 @@ function resolveProviderCellFromValue(provider, providerValue, tier, cellSource,
|
|
|
343
304
|
warnings: [],
|
|
344
305
|
};
|
|
345
306
|
}
|
|
307
|
+
if (isWorkflowDispatchCandidateLadder(cell)) {
|
|
308
|
+
const ceiling = cell.candidates.at(-1);
|
|
309
|
+
if (ceiling === undefined) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (typeof ceiling === 'string') {
|
|
313
|
+
return resolveRouteMatrixCell(provider, [ceiling], escalationLevel, cellSource);
|
|
314
|
+
}
|
|
315
|
+
const route = isWorkflowDispatchFallbackRoute(ceiling)
|
|
316
|
+
? ceiling.route
|
|
317
|
+
: [ceiling];
|
|
318
|
+
return resolveRouteMatrixCell(provider, route, escalationLevel, cellSource);
|
|
319
|
+
}
|
|
346
320
|
return resolveRouteMatrixCell(provider, cell, escalationLevel, cellSource);
|
|
347
321
|
}
|
|
348
322
|
function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix, escalationLevel) {
|
|
@@ -370,6 +344,214 @@ function resolveProviderMatrixCell(provider, tier, resolvedConfig, projectMatrix
|
|
|
370
344
|
}
|
|
371
345
|
return selected;
|
|
372
346
|
}
|
|
347
|
+
function resolveProviderMatrixCellDefinition(provider, tier, resolvedConfig, projectMatrix) {
|
|
348
|
+
let selected = null;
|
|
349
|
+
const layers = [
|
|
350
|
+
{
|
|
351
|
+
source: 'user-config',
|
|
352
|
+
providers: resolvedConfig.user.workflow?.dispatchCeiling?.providers,
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
source: 'repo-config',
|
|
356
|
+
providers: resolvedConfig.shared.workflow?.dispatchCeiling?.providers,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
source: 'local-config',
|
|
360
|
+
providers: resolvedConfig.local.workflow?.dispatchCeiling?.providers,
|
|
361
|
+
},
|
|
362
|
+
{ source: 'project-state', providers: projectMatrix },
|
|
363
|
+
];
|
|
364
|
+
for (const layer of layers) {
|
|
365
|
+
const providerValue = layer.providers?.[provider];
|
|
366
|
+
if (typeof providerValue === 'string') {
|
|
367
|
+
selected = null;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const cell = providerValue?.[tier];
|
|
371
|
+
if (cell !== undefined) {
|
|
372
|
+
selected = { cell, cellSource: layer.source };
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return selected;
|
|
376
|
+
}
|
|
377
|
+
function candidateRoute(candidate) {
|
|
378
|
+
return isWorkflowDispatchFallbackRoute(candidate)
|
|
379
|
+
? candidate.route
|
|
380
|
+
: [candidate];
|
|
381
|
+
}
|
|
382
|
+
function candidatePrimaryTarget(provider, tier, candidate) {
|
|
383
|
+
const route = candidateRoute(candidate);
|
|
384
|
+
const entry = route[0];
|
|
385
|
+
if (entry === undefined) {
|
|
386
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: fallback routes cannot be empty.`);
|
|
387
|
+
}
|
|
388
|
+
if (typeof entry !== 'string') {
|
|
389
|
+
const validation = validateDispatchRouteTarget(provider, entry);
|
|
390
|
+
if (!validation.valid) {
|
|
391
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: ${validation.reason}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const target = typeof entry === 'string'
|
|
395
|
+
? routeTargetFromBareValue(provider, entry, 0, route.length)
|
|
396
|
+
: routeTargetFromObject(provider, entry, 0, route.length);
|
|
397
|
+
if (target.model && isDirectDispatchRoleName(target.model)) {
|
|
398
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: direct dispatch role names are not candidate models.`);
|
|
399
|
+
}
|
|
400
|
+
const targetAdapter = getCeilingAdapter(target.harness);
|
|
401
|
+
if (targetAdapter.mechanism === 'pinned-variant' &&
|
|
402
|
+
(!target.model ||
|
|
403
|
+
!target.effort ||
|
|
404
|
+
!CODEX_VALUES.includes(target.effort))) {
|
|
405
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: Codex candidates require a model and supported effort.`);
|
|
406
|
+
}
|
|
407
|
+
if (targetAdapter.mechanism === 'model-arg' && !target.model) {
|
|
408
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: model-argument candidates require a model.`);
|
|
409
|
+
}
|
|
410
|
+
return target;
|
|
411
|
+
}
|
|
412
|
+
function candidateTargetKey(target) {
|
|
413
|
+
return JSON.stringify([
|
|
414
|
+
target.harness,
|
|
415
|
+
target.model ?? null,
|
|
416
|
+
target.effort ?? null,
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
function assertCandidateOrder(provider, tier, ladder) {
|
|
420
|
+
if (ladder.candidates.length === 0) {
|
|
421
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: candidates cannot be empty.`);
|
|
422
|
+
}
|
|
423
|
+
const targets = [];
|
|
424
|
+
const seen = new Set();
|
|
425
|
+
let previousClaudeRank = null;
|
|
426
|
+
const codexRanksByModel = new Map();
|
|
427
|
+
for (const candidate of ladder.candidates) {
|
|
428
|
+
const target = candidatePrimaryTarget(provider, tier, candidate);
|
|
429
|
+
const key = candidateTargetKey(target);
|
|
430
|
+
if (seen.has(key)) {
|
|
431
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: duplicate candidate ${key}.`);
|
|
432
|
+
}
|
|
433
|
+
seen.add(key);
|
|
434
|
+
if (target.harness === 'claude' && target.model) {
|
|
435
|
+
const rank = CLAUDE_TIER_ORDER.indexOf(target.model);
|
|
436
|
+
if (rank < 0) {
|
|
437
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: unsupported Claude model ${JSON.stringify(target.model)}.`);
|
|
438
|
+
}
|
|
439
|
+
if (previousClaudeRank !== null && rank < previousClaudeRank) {
|
|
440
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: Claude candidates must be nondecreasing.`);
|
|
441
|
+
}
|
|
442
|
+
previousClaudeRank = rank;
|
|
443
|
+
}
|
|
444
|
+
if (target.harness === 'codex' && target.model && target.effort) {
|
|
445
|
+
const rank = CODEX_VALUES.indexOf(target.effort);
|
|
446
|
+
const previousRank = codexRanksByModel.get(target.model);
|
|
447
|
+
if (previousRank !== undefined && rank < previousRank) {
|
|
448
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: Codex efforts for ${target.model} must be nondecreasing.`);
|
|
449
|
+
}
|
|
450
|
+
codexRanksByModel.set(target.model, rank);
|
|
451
|
+
}
|
|
452
|
+
targets.push(target);
|
|
453
|
+
}
|
|
454
|
+
return targets;
|
|
455
|
+
}
|
|
456
|
+
function assertTierCeilingsNondecreasing(provider, previous, current, tier) {
|
|
457
|
+
if (!previous || previous.harness !== current.harness) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (current.harness === 'claude' &&
|
|
461
|
+
previous.model &&
|
|
462
|
+
current.model &&
|
|
463
|
+
CLAUDE_TIER_ORDER.indexOf(current.model) <
|
|
464
|
+
CLAUDE_TIER_ORDER.indexOf(previous.model)) {
|
|
465
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: named tier ceilings must be nondecreasing.`);
|
|
466
|
+
}
|
|
467
|
+
if (current.harness === 'codex' &&
|
|
468
|
+
previous.model === current.model &&
|
|
469
|
+
previous.effort &&
|
|
470
|
+
current.effort &&
|
|
471
|
+
CODEX_VALUES.indexOf(current.effort) <
|
|
472
|
+
CODEX_VALUES.indexOf(previous.effort)) {
|
|
473
|
+
throw new Error(`Malformed ${provider} candidate ordering in ${tier}: named tier ceilings must be nondecreasing.`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function requestedCandidateMatches(provider, requested, target) {
|
|
477
|
+
if (target.harness !== provider || target.model !== requested.model) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
return provider !== 'codex' || target.effort === requested.effort;
|
|
481
|
+
}
|
|
482
|
+
function formatRequestedCandidate(provider, requested) {
|
|
483
|
+
return provider === 'codex'
|
|
484
|
+
? `${requested.model}/${requested.effort}`
|
|
485
|
+
: requested.model;
|
|
486
|
+
}
|
|
487
|
+
function resolveRequestedMatrixCandidate(provider, requested, ceilingTier, resolvedConfig, projectMatrix, escalationLevel) {
|
|
488
|
+
const ceilingIndex = ceilingTier === null
|
|
489
|
+
? VALID_DISPATCH_MATRIX_TIERS.length - 1
|
|
490
|
+
: VALID_DISPATCH_MATRIX_TIERS.indexOf(ceilingTier);
|
|
491
|
+
let ceilingCellFound = ceilingTier === null;
|
|
492
|
+
let previousCeiling = null;
|
|
493
|
+
let allowedMatch;
|
|
494
|
+
let foundAboveCeiling = false;
|
|
495
|
+
for (const [tierIndex, tier] of VALID_DISPATCH_MATRIX_TIERS.entries()) {
|
|
496
|
+
const definition = resolveProviderMatrixCellDefinition(provider, tier, resolvedConfig, projectMatrix);
|
|
497
|
+
if (!definition) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (tier === ceilingTier) {
|
|
501
|
+
ceilingCellFound = true;
|
|
502
|
+
}
|
|
503
|
+
const ladder = toWorkflowDispatchCandidateLadder(definition.cell);
|
|
504
|
+
const targets = assertCandidateOrder(provider, tier, ladder);
|
|
505
|
+
const currentCeiling = targets.at(-1);
|
|
506
|
+
assertTierCeilingsNondecreasing(provider, previousCeiling, currentCeiling, tier);
|
|
507
|
+
previousCeiling = currentCeiling;
|
|
508
|
+
for (const [candidateIndex, target] of targets.entries()) {
|
|
509
|
+
if (!requestedCandidateMatches(provider, requested, target)) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const candidate = ladder.candidates[candidateIndex];
|
|
513
|
+
const routeSignature = JSON.stringify(candidateRoute(candidate));
|
|
514
|
+
if (tierIndex > ceilingIndex) {
|
|
515
|
+
foundAboveCeiling = true;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (allowedMatch && allowedMatch.routeSignature !== routeSignature) {
|
|
519
|
+
throw new Error(`Ambiguous ${provider} candidate ${formatRequestedCandidate(provider, requested)} is configured with multiple routes at or below the ${ceilingTier ?? 'uncapped'} ceiling.`);
|
|
520
|
+
}
|
|
521
|
+
allowedMatch ??= {
|
|
522
|
+
tier,
|
|
523
|
+
candidateIndex,
|
|
524
|
+
candidate,
|
|
525
|
+
cellSource: definition.cellSource,
|
|
526
|
+
routeSignature,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!ceilingCellFound) {
|
|
531
|
+
throw new Error(`Exact candidate selection requires a configured ${ceilingTier} candidate ladder for ${provider}.`);
|
|
532
|
+
}
|
|
533
|
+
if (!allowedMatch) {
|
|
534
|
+
const label = formatRequestedCandidate(provider, requested);
|
|
535
|
+
if (foundAboveCeiling) {
|
|
536
|
+
throw new Error(`${provider} candidate ${label} is above the configured ${ceilingTier} ceiling.`);
|
|
537
|
+
}
|
|
538
|
+
throw new Error(`${provider} candidate ${label} is not present in the configured ${provider} candidate ladders.`);
|
|
539
|
+
}
|
|
540
|
+
const resolution = resolveRouteMatrixCell(provider, candidateRoute(allowedMatch.candidate), escalationLevel, allowedMatch.cellSource);
|
|
541
|
+
if (!resolution || resolution.value === null) {
|
|
542
|
+
throw new Error(`Configured ${provider} candidate ${formatRequestedCandidate(provider, requested)} cannot compile to an exact dispatch target.`);
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
resolution: {
|
|
546
|
+
...resolution,
|
|
547
|
+
selectionBranch: resolution.selectionBranch === 'escalation-target'
|
|
548
|
+
? 'escalation-target'
|
|
549
|
+
: 'candidate-requested',
|
|
550
|
+
},
|
|
551
|
+
candidateTier: allowedMatch.tier,
|
|
552
|
+
candidateIndex: allowedMatch.candidateIndex,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
373
555
|
function readProjectDispatchPolicy(provider, content) {
|
|
374
556
|
const frontmatter = getFrontmatterBlock(content);
|
|
375
557
|
if (!frontmatter) {
|
|
@@ -394,9 +576,11 @@ function readProjectDispatchPolicy(provider, content) {
|
|
|
394
576
|
source: 'project-state',
|
|
395
577
|
preset: null,
|
|
396
578
|
matrix: parsedMatrix.matrix,
|
|
579
|
+
matrixCompatibility: parsedMatrix.compatibilityMatrix,
|
|
397
580
|
cellSource: null,
|
|
398
581
|
target: null,
|
|
399
582
|
selectionBranch: 'inherit',
|
|
583
|
+
candidateIndex: null,
|
|
400
584
|
warnings: parsedMatrix.warnings,
|
|
401
585
|
};
|
|
402
586
|
}
|
|
@@ -417,9 +601,11 @@ function readProjectDispatchPolicy(provider, content) {
|
|
|
417
601
|
source: 'project-state',
|
|
418
602
|
preset: policyValue,
|
|
419
603
|
matrix: parsedMatrix.matrix,
|
|
604
|
+
matrixCompatibility: parsedMatrix.compatibilityMatrix,
|
|
420
605
|
cellSource: null,
|
|
421
606
|
target: null,
|
|
422
607
|
selectionBranch: 'prompt-persisted',
|
|
608
|
+
candidateIndex: null,
|
|
423
609
|
warnings: parsedMatrix.warnings,
|
|
424
610
|
};
|
|
425
611
|
}
|
|
@@ -444,9 +630,11 @@ function readProjectDispatchPolicy(provider, content) {
|
|
|
444
630
|
source: 'project-state',
|
|
445
631
|
preset: policyValue,
|
|
446
632
|
matrix: parsedMatrix.matrix,
|
|
633
|
+
matrixCompatibility: parsedMatrix.compatibilityMatrix,
|
|
447
634
|
cellSource: 'project-state',
|
|
448
635
|
target: null,
|
|
449
636
|
selectionBranch: 'prompt-persisted',
|
|
637
|
+
candidateIndex: null,
|
|
450
638
|
warnings: parsedMatrix.warnings,
|
|
451
639
|
};
|
|
452
640
|
}
|
|
@@ -477,6 +665,7 @@ function readLegacyProjectDispatchCeiling(provider, parsed) {
|
|
|
477
665
|
cellSource: 'project-state',
|
|
478
666
|
target: null,
|
|
479
667
|
selectionBranch: 'prompt-persisted',
|
|
668
|
+
candidateIndex: null,
|
|
480
669
|
warnings: [],
|
|
481
670
|
};
|
|
482
671
|
}
|
|
@@ -545,9 +734,11 @@ function stripConfigCandidateSource(candidate) {
|
|
|
545
734
|
source: candidate.source,
|
|
546
735
|
preset: candidate.preset,
|
|
547
736
|
matrix: candidate.matrix,
|
|
737
|
+
matrixCompatibility: candidate.matrixCompatibility,
|
|
548
738
|
cellSource: candidate.cellSource,
|
|
549
739
|
target: candidate.target,
|
|
550
740
|
selectionBranch: candidate.selectionBranch,
|
|
741
|
+
candidateIndex: candidate.candidateIndex,
|
|
551
742
|
warnings: candidate.warnings,
|
|
552
743
|
};
|
|
553
744
|
}
|
|
@@ -570,6 +761,7 @@ function readResolvedConfigPolicyCandidate(provider, resolvedConfig) {
|
|
|
570
761
|
cellSource: null,
|
|
571
762
|
target: null,
|
|
572
763
|
selectionBranch: 'inherit',
|
|
764
|
+
candidateIndex: null,
|
|
573
765
|
warnings: [],
|
|
574
766
|
configSource: modeEntry.source,
|
|
575
767
|
};
|
|
@@ -595,6 +787,7 @@ function readResolvedConfigPolicyCandidate(provider, resolvedConfig) {
|
|
|
595
787
|
cellSource: null,
|
|
596
788
|
target: null,
|
|
597
789
|
selectionBranch: 'prompt-persisted',
|
|
790
|
+
candidateIndex: null,
|
|
598
791
|
warnings: [],
|
|
599
792
|
configSource,
|
|
600
793
|
};
|
|
@@ -625,6 +818,7 @@ function readResolvedLegacyConfigCeilingCandidate(provider, resolvedConfig) {
|
|
|
625
818
|
cellSource: source,
|
|
626
819
|
target: null,
|
|
627
820
|
selectionBranch: 'prompt-persisted',
|
|
821
|
+
candidateIndex: null,
|
|
628
822
|
warnings: [],
|
|
629
823
|
configSource: entry.source,
|
|
630
824
|
};
|
|
@@ -645,7 +839,13 @@ function readResolvedConfigCeiling(provider, resolvedConfig) {
|
|
|
645
839
|
return stripConfigCandidateSource(winner);
|
|
646
840
|
}
|
|
647
841
|
function normalizeRole(value) {
|
|
648
|
-
|
|
842
|
+
if (value === undefined) {
|
|
843
|
+
return 'implementer';
|
|
844
|
+
}
|
|
845
|
+
if (value === 'implementer' || value === 'reviewer') {
|
|
846
|
+
return value;
|
|
847
|
+
}
|
|
848
|
+
throw new Error(`Invalid dispatch role ${JSON.stringify(value)}. Expected implementer or reviewer.`);
|
|
649
849
|
}
|
|
650
850
|
function providerValueOrder(provider) {
|
|
651
851
|
if (provider === 'codex') {
|
|
@@ -674,6 +874,55 @@ function normalizePreferredValue(provider, value) {
|
|
|
674
874
|
}
|
|
675
875
|
return normalized;
|
|
676
876
|
}
|
|
877
|
+
function normalizeCeilingTier(value, role) {
|
|
878
|
+
if (value === undefined) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
const normalized = value.trim();
|
|
882
|
+
if (!VALID_DISPATCH_MATRIX_TIERS.includes(normalized)) {
|
|
883
|
+
throw new Error(`Invalid invocation ceiling tier "${normalized}". Valid tiers: ${VALID_DISPATCH_MATRIX_TIERS.join(', ')}.`);
|
|
884
|
+
}
|
|
885
|
+
if (role === 'reviewer') {
|
|
886
|
+
throw new Error('Invocation ceiling tiers are only supported for implementer/fix task dispatch; reviewers use the configured review ceiling.');
|
|
887
|
+
}
|
|
888
|
+
return normalized;
|
|
889
|
+
}
|
|
890
|
+
function normalizeRequestedCandidate(provider, role, options) {
|
|
891
|
+
const model = options.candidateModel?.trim() ?? '';
|
|
892
|
+
const effort = options.candidateEffort?.trim() ?? '';
|
|
893
|
+
if (!model && !effort) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
if (options.preferred?.trim()) {
|
|
897
|
+
throw new Error('Exact candidate flags cannot be combined with --preferred; use one selection path.');
|
|
898
|
+
}
|
|
899
|
+
if (role === 'reviewer') {
|
|
900
|
+
throw new Error('Reviewer candidate requests are not supported; reviewers use the configured review ceiling.');
|
|
901
|
+
}
|
|
902
|
+
if (!model) {
|
|
903
|
+
throw new Error('--candidate-model is required for an exact candidate.');
|
|
904
|
+
}
|
|
905
|
+
if (isDirectDispatchRoleName(model)) {
|
|
906
|
+
throw new Error('Direct dispatch role names are not candidate models; request the configured provider model and effort instead.');
|
|
907
|
+
}
|
|
908
|
+
if (provider === 'codex') {
|
|
909
|
+
if (!effort) {
|
|
910
|
+
throw new Error('--candidate-effort is required for an exact Codex candidate.');
|
|
911
|
+
}
|
|
912
|
+
if (!CODEX_VALUES.includes(effort)) {
|
|
913
|
+
throw new Error(`Invalid Codex candidate effort "${effort}". Valid values: ${CODEX_VALUES.join(', ')}.`);
|
|
914
|
+
}
|
|
915
|
+
return { model, effort };
|
|
916
|
+
}
|
|
917
|
+
if (effort) {
|
|
918
|
+
throw new Error(`--candidate-effort is only valid for Codex; ${provider} candidates use --candidate-model only.`);
|
|
919
|
+
}
|
|
920
|
+
if (provider === 'claude' &&
|
|
921
|
+
!CLAUDE_VALUES.includes(model)) {
|
|
922
|
+
throw new Error(`Invalid Claude candidate model "${model}". Valid values: ${CLAUDE_VALUES.join(', ')}.`);
|
|
923
|
+
}
|
|
924
|
+
return { model };
|
|
925
|
+
}
|
|
677
926
|
function normalizeEscalationLevel(value) {
|
|
678
927
|
if (value === undefined) {
|
|
679
928
|
return 0;
|
|
@@ -696,6 +945,11 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
|
|
|
696
945
|
policy: policy.policy,
|
|
697
946
|
cellSource: policy.cellSource,
|
|
698
947
|
target: policy.target,
|
|
948
|
+
requestedCandidate: policy.requestedCandidate ?? null,
|
|
949
|
+
candidateTier: policy.candidateTier ?? null,
|
|
950
|
+
candidateIndex: policy.candidateIndex,
|
|
951
|
+
ceilingTier: policy.ceilingTier ?? policyTier(policy.policy),
|
|
952
|
+
ceilingTarget: policy.ceilingTarget ?? policy.target,
|
|
699
953
|
};
|
|
700
954
|
if (policy.mode === 'inherit') {
|
|
701
955
|
return {
|
|
@@ -709,6 +963,21 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
|
|
|
709
963
|
target: null,
|
|
710
964
|
};
|
|
711
965
|
}
|
|
966
|
+
if (policy.requestedCandidate) {
|
|
967
|
+
const selectedValue = policy.target
|
|
968
|
+
? dispatchValueFromRouteTarget(policy.target)
|
|
969
|
+
: null;
|
|
970
|
+
return {
|
|
971
|
+
...baseSelection,
|
|
972
|
+
preferredValue: null,
|
|
973
|
+
selectedValue,
|
|
974
|
+
capped: false,
|
|
975
|
+
selectionMode: selectedValue ? 'candidate' : 'unresolved',
|
|
976
|
+
selectionBranch: policy.selectionBranch,
|
|
977
|
+
family: selectionFamily(provider, selectedValue, policy.target),
|
|
978
|
+
target: policy.target,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
712
981
|
if (policy.policy === 'uncapped') {
|
|
713
982
|
const targetValue = policy.target
|
|
714
983
|
? dispatchValueFromRouteTarget(policy.target)
|
|
@@ -769,6 +1038,10 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
|
|
|
769
1038
|
}
|
|
770
1039
|
const selectedIndex = Math.min(preferredIndex, ceilingIndex);
|
|
771
1040
|
const selectedValue = order[selectedIndex];
|
|
1041
|
+
const targetValue = policy.target
|
|
1042
|
+
? dispatchValueFromRouteTarget(policy.target)
|
|
1043
|
+
: null;
|
|
1044
|
+
const selectedTarget = targetValue === selectedValue ? policy.target : null;
|
|
772
1045
|
return {
|
|
773
1046
|
...baseSelection,
|
|
774
1047
|
preferredValue,
|
|
@@ -776,8 +1049,8 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
|
|
|
776
1049
|
capped: preferredIndex > ceilingIndex,
|
|
777
1050
|
selectionMode: 'capped',
|
|
778
1051
|
selectionBranch: policy.selectionBranch,
|
|
779
|
-
family: selectionFamily(provider, selectedValue,
|
|
780
|
-
target:
|
|
1052
|
+
family: selectionFamily(provider, selectedValue, selectedTarget),
|
|
1053
|
+
target: selectedTarget,
|
|
781
1054
|
};
|
|
782
1055
|
}
|
|
783
1056
|
function hasCodexVariantDispatchArgs(dispatchArgs) {
|
|
@@ -835,6 +1108,11 @@ function buildProviderResolution(provider, policy, role, orchestratorTier, prefe
|
|
|
835
1108
|
selection: {
|
|
836
1109
|
role,
|
|
837
1110
|
preferredValue,
|
|
1111
|
+
requestedCandidate: null,
|
|
1112
|
+
candidateTier: null,
|
|
1113
|
+
candidateIndex: null,
|
|
1114
|
+
ceilingTier: null,
|
|
1115
|
+
ceilingTarget: null,
|
|
838
1116
|
selectedValue: null,
|
|
839
1117
|
capped: false,
|
|
840
1118
|
selectionMode: 'unresolved',
|
|
@@ -933,9 +1211,11 @@ async function resolveDispatchCeiling(context, dependencies, options) {
|
|
|
933
1211
|
const providerDefaultEffort = provider === 'codex'
|
|
934
1212
|
? await resolveCodexProviderDefaultEffort(repoRoot, context, dependencies)
|
|
935
1213
|
: 'not-applicable';
|
|
1214
|
+
const requestedCandidate = normalizeRequestedCandidate(provider, role, options);
|
|
936
1215
|
const preferredValue = normalizePreferredValue(provider, options.preferred);
|
|
1216
|
+
const ceilingTier = normalizeCeilingTier(options.ceilingTier, role);
|
|
937
1217
|
const escalationLevel = normalizeEscalationLevel(options.escalationLevel);
|
|
938
|
-
const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue);
|
|
1218
|
+
const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue, requestedCandidate, ceilingTier);
|
|
939
1219
|
for (const warning of resolvedValue?.warnings ?? []) {
|
|
940
1220
|
context.logger.warn(warning);
|
|
941
1221
|
}
|
|
@@ -944,6 +1224,32 @@ async function resolveDispatchCeiling(context, dependencies, options) {
|
|
|
944
1224
|
[provider]: providerResolution,
|
|
945
1225
|
};
|
|
946
1226
|
if (resolvedValue) {
|
|
1227
|
+
const incompleteManagedPreflight = options.preflight === true &&
|
|
1228
|
+
resolvedValue.mode === 'managed' &&
|
|
1229
|
+
providerResolution.mode === 'advisory' &&
|
|
1230
|
+
providerResolution.selection.target?.crossHarness !== true &&
|
|
1231
|
+
providerResolution.selection.selectionMode !== 'no-review-target';
|
|
1232
|
+
if (incompleteManagedPreflight) {
|
|
1233
|
+
const shouldBlock = options.nonInteractive === true ||
|
|
1234
|
+
isNonInteractiveEnv(dependencies.processEnv) ||
|
|
1235
|
+
(!context.interactive && !context.json);
|
|
1236
|
+
const message = shouldBlock ? blockMessage(provider) : undefined;
|
|
1237
|
+
return {
|
|
1238
|
+
status: shouldBlock ? 'blocked' : 'unresolved',
|
|
1239
|
+
provider,
|
|
1240
|
+
value: resolvedValue.value,
|
|
1241
|
+
policyMode: resolvedValue.mode,
|
|
1242
|
+
policy: resolvedValue.policy,
|
|
1243
|
+
source: resolvedValue.source,
|
|
1244
|
+
preset: resolvedValue.preset,
|
|
1245
|
+
unresolved: true,
|
|
1246
|
+
projectPath,
|
|
1247
|
+
providerDefaultEffort,
|
|
1248
|
+
matrix: resolvedValue.matrixCompatibility ?? resolvedValue.matrix,
|
|
1249
|
+
providers,
|
|
1250
|
+
message,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
947
1253
|
return {
|
|
948
1254
|
status: 'resolved',
|
|
949
1255
|
provider,
|
|
@@ -955,7 +1261,7 @@ async function resolveDispatchCeiling(context, dependencies, options) {
|
|
|
955
1261
|
unresolved: false,
|
|
956
1262
|
projectPath,
|
|
957
1263
|
providerDefaultEffort,
|
|
958
|
-
matrix: resolvedValue.matrix,
|
|
1264
|
+
matrix: resolvedValue.matrixCompatibility ?? resolvedValue.matrix,
|
|
959
1265
|
providers,
|
|
960
1266
|
};
|
|
961
1267
|
}
|
|
@@ -985,16 +1291,63 @@ async function resolveDispatchCeiling(context, dependencies, options) {
|
|
|
985
1291
|
* Never reads the preset label for dispatch — the preset is surfaced as
|
|
986
1292
|
* provenance only.
|
|
987
1293
|
*/
|
|
988
|
-
async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue) {
|
|
1294
|
+
async function resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies, escalationLevel, role, preferredValue, requestedCandidate, invocationCeilingTier) {
|
|
989
1295
|
const configCeiling = readResolvedConfigCeiling(provider, resolvedConfig);
|
|
990
1296
|
const projectCeiling = await resolveProjectStateCeiling(provider, projectPath, dependencies);
|
|
991
|
-
|
|
1297
|
+
let baseCeiling = configCeiling ?? projectCeiling;
|
|
992
1298
|
if (!baseCeiling) {
|
|
993
1299
|
return null;
|
|
994
1300
|
}
|
|
1301
|
+
if (invocationCeilingTier) {
|
|
1302
|
+
baseCeiling = {
|
|
1303
|
+
...baseCeiling,
|
|
1304
|
+
mode: 'managed',
|
|
1305
|
+
policy: invocationCeilingTier,
|
|
1306
|
+
value: compiledPolicyValueForProvider(provider, compileDispatchPolicyPreset(invocationCeilingTier)),
|
|
1307
|
+
source: 'invocation',
|
|
1308
|
+
preset: invocationCeilingTier,
|
|
1309
|
+
matrix: projectCeiling?.matrix ?? baseCeiling.matrix,
|
|
1310
|
+
matrixCompatibility: projectCeiling?.matrixCompatibility ?? baseCeiling.matrixCompatibility,
|
|
1311
|
+
cellSource: null,
|
|
1312
|
+
target: null,
|
|
1313
|
+
selectionBranch: 'prompt-persisted',
|
|
1314
|
+
ceilingTier: invocationCeilingTier,
|
|
1315
|
+
ceilingTarget: null,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
995
1318
|
if (baseCeiling.mode === 'inherit') {
|
|
1319
|
+
if (requestedCandidate) {
|
|
1320
|
+
throw new Error('Exact candidate selection requires a managed dispatch policy and configured candidate ladder.');
|
|
1321
|
+
}
|
|
996
1322
|
return baseCeiling;
|
|
997
1323
|
}
|
|
1324
|
+
if (requestedCandidate) {
|
|
1325
|
+
if (baseCeiling.policy === 'legacy-ceiling') {
|
|
1326
|
+
throw new Error('Exact candidate selection requires a configured candidate ladder; legacy scalar dispatch ceilings support --preferred only during migration.');
|
|
1327
|
+
}
|
|
1328
|
+
const ceilingTier = policyTier(baseCeiling.policy);
|
|
1329
|
+
const selected = resolveRequestedMatrixCandidate(provider, requestedCandidate, ceilingTier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel);
|
|
1330
|
+
const ceilingCell = ceilingTier
|
|
1331
|
+
? resolveProviderMatrixCell(provider, ceilingTier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel)
|
|
1332
|
+
: null;
|
|
1333
|
+
return {
|
|
1334
|
+
...baseCeiling,
|
|
1335
|
+
value: ceilingCell?.value ?? baseCeiling.value,
|
|
1336
|
+
cellSource: selected.resolution.cellSource,
|
|
1337
|
+
target: selected.resolution.target,
|
|
1338
|
+
selectionBranch: selected.resolution.selectionBranch,
|
|
1339
|
+
warnings: [
|
|
1340
|
+
...baseCeiling.warnings,
|
|
1341
|
+
...(ceilingCell?.warnings ?? []),
|
|
1342
|
+
...selected.resolution.warnings,
|
|
1343
|
+
],
|
|
1344
|
+
requestedCandidate,
|
|
1345
|
+
candidateTier: selected.candidateTier,
|
|
1346
|
+
candidateIndex: selected.candidateIndex,
|
|
1347
|
+
ceilingTarget: ceilingCell?.target ?? null,
|
|
1348
|
+
...(ceilingTier ? { ceilingTier } : {}),
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
998
1351
|
if (baseCeiling.policy === 'uncapped') {
|
|
999
1352
|
const preferredTier = uncappedPreferredTier(provider, role, preferredValue);
|
|
1000
1353
|
if (preferredTier) {
|
|
@@ -1014,6 +1367,30 @@ async function resolveCeilingValue(provider, resolvedConfig, projectPath, depend
|
|
|
1014
1367
|
const tier = policyTier(baseCeiling.policy);
|
|
1015
1368
|
const matrixCell = resolveProviderMatrixCell(provider, tier, resolvedConfig, projectCeiling?.matrix ?? null, escalationLevel);
|
|
1016
1369
|
if (matrixCell) {
|
|
1370
|
+
if (provider === 'codex' &&
|
|
1371
|
+
role === 'implementer' &&
|
|
1372
|
+
preferredValue !== null &&
|
|
1373
|
+
matrixCell.value !== null) {
|
|
1374
|
+
const order = providerValueOrder(provider);
|
|
1375
|
+
const preferredIndex = order?.indexOf(preferredValue) ?? -1;
|
|
1376
|
+
const ceilingIndex = order?.indexOf(matrixCell.value) ?? -1;
|
|
1377
|
+
if (order && preferredIndex >= 0 && ceilingIndex >= 0) {
|
|
1378
|
+
const selectedValue = order[Math.min(preferredIndex, ceilingIndex)];
|
|
1379
|
+
if (selectedValue !== matrixCell.value) {
|
|
1380
|
+
const selectedTarget = matrixCell.target?.harness === 'codex'
|
|
1381
|
+
? { ...matrixCell.target, effort: selectedValue }
|
|
1382
|
+
: matrixCell.target;
|
|
1383
|
+
return {
|
|
1384
|
+
...baseCeiling,
|
|
1385
|
+
value: matrixCell.value,
|
|
1386
|
+
cellSource: matrixCell.cellSource,
|
|
1387
|
+
target: selectedTarget,
|
|
1388
|
+
selectionBranch: matrixCell.selectionBranch,
|
|
1389
|
+
warnings: [...baseCeiling.warnings, ...matrixCell.warnings],
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1017
1394
|
return {
|
|
1018
1395
|
...baseCeiling,
|
|
1019
1396
|
value: matrixCell.value,
|
|
@@ -1049,6 +1426,15 @@ function writeHumanResolution(context, resolution) {
|
|
|
1049
1426
|
if (providerResolution) {
|
|
1050
1427
|
context.logger.info(`Mode: ${providerResolution.mode} (${providerResolution.mechanism})`);
|
|
1051
1428
|
context.logger.info(`Selection: ${providerResolution.selection.selectionMode}`);
|
|
1429
|
+
if (providerResolution.selection.requestedCandidate) {
|
|
1430
|
+
const requested = providerResolution.selection.requestedCandidate;
|
|
1431
|
+
context.logger.info(`Requested candidate: model=${requested.model}${requested.effort ? ` effort=${requested.effort}` : ''}`);
|
|
1432
|
+
context.logger.info(`Candidate tier: ${providerResolution.selection.candidateTier ?? 'none'}; ceiling tier: ${providerResolution.selection.ceilingTier ?? 'uncapped'}`);
|
|
1433
|
+
const ceilingTarget = providerResolution.selection.ceilingTarget;
|
|
1434
|
+
if (ceilingTarget) {
|
|
1435
|
+
context.logger.info(`Effective ceiling target: model=${ceilingTarget.model ?? 'none'}${ceilingTarget.effort ? ` effort=${ceilingTarget.effort}` : ''}`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1052
1438
|
if (providerResolution.target) {
|
|
1053
1439
|
const details = [
|
|
1054
1440
|
`harness=${providerResolution.target.harness}`,
|
|
@@ -1100,14 +1486,91 @@ function writeHumanResolution(context, resolution) {
|
|
|
1100
1486
|
}
|
|
1101
1487
|
}
|
|
1102
1488
|
}
|
|
1489
|
+
function reportRole(action) {
|
|
1490
|
+
if (action === 'review') {
|
|
1491
|
+
return 'reviewer';
|
|
1492
|
+
}
|
|
1493
|
+
return action === 'fix' ? 'fix' : 'implementer';
|
|
1494
|
+
}
|
|
1495
|
+
function reportControl(axis, dispatchArgs, reason) {
|
|
1496
|
+
if (axis.startsWith('selected:')) {
|
|
1497
|
+
return {
|
|
1498
|
+
value: axis.slice('selected:'.length),
|
|
1499
|
+
mechanism: dispatchArgs && 'variant' in dispatchArgs
|
|
1500
|
+
? 'materialized-role'
|
|
1501
|
+
: 'task-model-argument',
|
|
1502
|
+
reason,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
if (axis === 'inherited') {
|
|
1506
|
+
return { value: null, mechanism: 'host-inherited', reason };
|
|
1507
|
+
}
|
|
1508
|
+
if (axis === 'provider-default') {
|
|
1509
|
+
return { value: null, mechanism: 'provider-default', reason };
|
|
1510
|
+
}
|
|
1511
|
+
if (axis === 'not-applicable') {
|
|
1512
|
+
return { value: null, mechanism: 'not-applicable', reason };
|
|
1513
|
+
}
|
|
1514
|
+
return { value: null, mechanism: 'base-role', reason };
|
|
1515
|
+
}
|
|
1516
|
+
function buildResolutionReport(resolution, options) {
|
|
1517
|
+
const hasScope = options.reportScope !== undefined;
|
|
1518
|
+
const hasAction = options.reportAction !== undefined;
|
|
1519
|
+
if (!hasScope && !hasAction) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
if (!hasScope || !hasAction) {
|
|
1523
|
+
throw new Error('--report-scope and --report-action must be provided together.');
|
|
1524
|
+
}
|
|
1525
|
+
const scope = options.reportScope.trim();
|
|
1526
|
+
if (!scope) {
|
|
1527
|
+
throw new Error('--report-scope must be a non-empty value.');
|
|
1528
|
+
}
|
|
1529
|
+
const action = options.reportAction;
|
|
1530
|
+
const role = reportRole(action);
|
|
1531
|
+
const providerResolution = resolution.providers[resolution.provider];
|
|
1532
|
+
if (!providerResolution) {
|
|
1533
|
+
throw new Error(`Dispatch report resolution is missing provider data for "${resolution.provider}".`);
|
|
1534
|
+
}
|
|
1535
|
+
const expectedResolverRole = role === 'reviewer' ? 'reviewer' : 'implementer';
|
|
1536
|
+
if (providerResolution.selection.role !== expectedResolverRole) {
|
|
1537
|
+
throw new Error(`Invalid dispatch report action/role context: ${action}/${role} requires resolver role ${expectedResolverRole}, received ${providerResolution.selection.role}.`);
|
|
1538
|
+
}
|
|
1539
|
+
return buildDispatchReport({
|
|
1540
|
+
scope,
|
|
1541
|
+
action,
|
|
1542
|
+
role,
|
|
1543
|
+
resolution,
|
|
1544
|
+
requestedControls: {
|
|
1545
|
+
model: reportControl(providerResolution.modelAxis, providerResolution.dispatchArgs, 'Derived from the completed resolver model-axis result.'),
|
|
1546
|
+
effort: reportControl(providerResolution.effortAxis, providerResolution.dispatchArgs, 'Derived from the completed resolver effort-axis result.'),
|
|
1547
|
+
},
|
|
1548
|
+
configuredDefaults: {
|
|
1549
|
+
model: null,
|
|
1550
|
+
modelSource: null,
|
|
1551
|
+
effort: resolution.provider === 'codex' &&
|
|
1552
|
+
resolution.providerDefaultEffort !== 'unknown'
|
|
1553
|
+
? resolution.providerDefaultEffort
|
|
1554
|
+
: null,
|
|
1555
|
+
effortSource: resolution.provider === 'codex' &&
|
|
1556
|
+
resolution.providerDefaultEffort !== 'unknown'
|
|
1557
|
+
? 'codex-config'
|
|
1558
|
+
: null,
|
|
1559
|
+
},
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1103
1562
|
async function runDispatchCeilingResolve(context, dependencies, options) {
|
|
1104
1563
|
try {
|
|
1105
1564
|
const resolution = await resolveDispatchCeiling(context, dependencies, options);
|
|
1565
|
+
const dispatchReport = buildResolutionReport(resolution, options);
|
|
1106
1566
|
if (context.json) {
|
|
1107
|
-
context.logger.json(resolution);
|
|
1567
|
+
context.logger.json(dispatchReport ? { ...resolution, dispatchReport } : resolution);
|
|
1108
1568
|
}
|
|
1109
1569
|
else {
|
|
1110
1570
|
writeHumanResolution(context, resolution);
|
|
1571
|
+
if (dispatchReport) {
|
|
1572
|
+
context.logger.info(formatDispatchReport(dispatchReport));
|
|
1573
|
+
}
|
|
1111
1574
|
}
|
|
1112
1575
|
process.exitCode = resolution.status === 'blocked' ? 1 : 0;
|
|
1113
1576
|
}
|
|
@@ -1159,13 +1622,18 @@ export function createProjectDispatchCeilingCommand(overrides = {}) {
|
|
|
1159
1622
|
command.addCommand(new Command('resolve')
|
|
1160
1623
|
.description('Resolve dispatch policy for a provider')
|
|
1161
1624
|
.requiredOption('--provider <provider>', 'Provider name: codex, claude, or cursor are enforced; unregistered providers resolve as unsupported advisory')
|
|
1162
|
-
.
|
|
1625
|
+
.addOption(new Option('--role <role>', 'Dispatch role for variant compilation: implementer (default) or reviewer').choices(['implementer', 'reviewer']))
|
|
1163
1626
|
.option('--orchestrator-tier <tier>', 'Orchestrator tier, used to flag verify-on-upgrade for above-orchestrator requests')
|
|
1164
|
-
.option('--preferred <value>', '
|
|
1627
|
+
.option('--preferred <value>', 'Legacy preferred implementer/fix value before applying the resolved policy')
|
|
1628
|
+
.addOption(new Option('--ceiling-tier <tier>', 'Invocation-only named maximum tier; never persists configuration or project state').choices([...VALID_DISPATCH_MATRIX_TIERS]))
|
|
1629
|
+
.option('--candidate-model <model>', 'Exact configured implementer candidate model beneath the named ceiling')
|
|
1630
|
+
.option('--candidate-effort <effort>', 'Exact configured Codex candidate effort paired with --candidate-model')
|
|
1165
1631
|
.option('--escalation-level <level>', 'Ordered route entry to select; 0 is the floor and higher values advance through escalation targets')
|
|
1166
1632
|
.option('--project-path <path>', 'Read project-state policy from an explicit project path')
|
|
1167
1633
|
.option('--preflight', 'Treat unresolved non-interactive resolution as an implementation block')
|
|
1168
1634
|
.option('--non-interactive', 'Force non-interactive block behavior when the ceiling is unresolved')
|
|
1635
|
+
.option('--report-scope <scope>', 'Include Dispatch Report V1 for the explicit workflow scope')
|
|
1636
|
+
.addOption(new Option('--report-action <action>', 'Dispatch Report V1 action; must match the resolver role').choices(['implementation', 'fix', 'review']))
|
|
1169
1637
|
.option('--json', 'Output machine-readable JSON')
|
|
1170
1638
|
.action(async (options, cmd) => {
|
|
1171
1639
|
const globalOptions = readGlobalOptions(cmd);
|