@open-agent-toolkit/cli 0.1.48 → 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.
Files changed (40) hide show
  1. package/assets/docs/cli-utilities/configuration.md +41 -0
  2. package/assets/docs/cli-utilities/workflow-gates.md +17 -0
  3. package/assets/docs/provider-sync/providers.md +5 -1
  4. package/assets/docs/workflows/projects/dispatch-ceiling.md +59 -7
  5. package/assets/docs/workflows/projects/implementation-execution.md +34 -0
  6. package/assets/public-package-versions.json +4 -4
  7. package/assets/skills/oat-project-implement/SKILL.md +49 -22
  8. package/assets/skills/oat-project-review-provide/SKILL.md +18 -2
  9. package/assets/skills/oat-project-review-provide-remote/SKILL.md +28 -1
  10. package/dist/commands/config/index.d.ts +3 -0
  11. package/dist/commands/config/index.d.ts.map +1 -1
  12. package/dist/commands/config/index.js +70 -128
  13. package/dist/commands/doctor/index.d.ts +3 -0
  14. package/dist/commands/doctor/index.d.ts.map +1 -1
  15. package/dist/commands/doctor/index.js +57 -110
  16. package/dist/commands/gate/index.d.ts.map +1 -1
  17. package/dist/commands/gate/index.js +71 -0
  18. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  19. package/dist/commands/project/dispatch-ceiling/index.js +144 -112
  20. package/dist/config/dispatch-matrix.d.ts +59 -0
  21. package/dist/config/dispatch-matrix.d.ts.map +1 -0
  22. package/dist/config/dispatch-matrix.js +264 -0
  23. package/dist/config/oat-config.d.ts +2 -28
  24. package/dist/config/oat-config.d.ts.map +1 -1
  25. package/dist/config/oat-config.js +8 -144
  26. package/dist/config/resolve.d.ts.map +1 -1
  27. package/dist/config/resolve.js +2 -1
  28. package/dist/providers/identity/availability.d.ts +12 -0
  29. package/dist/providers/identity/availability.d.ts.map +1 -1
  30. package/dist/providers/identity/availability.js +72 -29
  31. package/dist/providers/identity/dispatch-report.d.ts +124 -0
  32. package/dist/providers/identity/dispatch-report.d.ts.map +1 -0
  33. package/dist/providers/identity/dispatch-report.js +285 -0
  34. package/dist/providers/identity/dispatch-validation.d.ts +28 -0
  35. package/dist/providers/identity/dispatch-validation.d.ts.map +1 -0
  36. package/dist/providers/identity/dispatch-validation.js +149 -0
  37. package/dist/providers/identity/stamp.d.ts +2 -0
  38. package/dist/providers/identity/stamp.d.ts.map +1 -1
  39. package/dist/providers/identity/stamp.js +3 -1
  40. package/package.json +2 -2
@@ -9,6 +9,7 @@ import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig
9
9
  import { resolveEffectiveConfig, resolveExecTargetViews, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
10
10
  import { dirExists, fileExists } from '../../fs/io.js';
11
11
  import { normalizeToPosixPath, resolveProjectRoot, validateRealPathWithinScope, } from '../../fs/paths.js';
12
+ import { buildDispatchReport, } from '../../providers/identity/dispatch-report.js';
12
13
  import { classifyModelFamily, } from '../../providers/identity/family.js';
13
14
  import { resolveIdentityConfidence, } from '../../providers/identity/provenance.js';
14
15
  import { parseDispatchStamps } from '../../providers/identity/stamp.js';
@@ -94,6 +95,62 @@ function createGateInvocationMetadata(runId, selected) {
94
95
  : {}),
95
96
  });
96
97
  }
98
+ function buildGateDispatchReport(invocation, scope) {
99
+ const report = buildDispatchReport({
100
+ scope,
101
+ action: 'review',
102
+ role: 'reviewer',
103
+ resolution: {
104
+ status: 'resolved',
105
+ provider: invocation.runtime,
106
+ value: null,
107
+ policyMode: null,
108
+ policy: null,
109
+ source: null,
110
+ providers: {
111
+ [invocation.runtime]: {
112
+ dispatchArgs: null,
113
+ selection: {
114
+ role: 'reviewer',
115
+ requestedCandidate: null,
116
+ candidateTier: null,
117
+ candidateIndex: null,
118
+ ceilingTier: null,
119
+ ceilingTarget: null,
120
+ selectedValue: null,
121
+ selectionMode: 'gate-invocation',
122
+ selectionBranch: 'gate-configured-invocation',
123
+ target: null,
124
+ cellSource: null,
125
+ },
126
+ },
127
+ },
128
+ },
129
+ requestedControls: {
130
+ model: {
131
+ value: null,
132
+ mechanism: 'base-role',
133
+ reason: 'Configured gate invocation is reported separately from runtime identity.',
134
+ },
135
+ effort: {
136
+ value: null,
137
+ mechanism: 'base-role',
138
+ reason: 'Configured gate invocation is reported separately from runtime identity.',
139
+ },
140
+ },
141
+ configuredDefaults: {
142
+ model: null,
143
+ modelSource: null,
144
+ effort: null,
145
+ effortSource: null,
146
+ },
147
+ gateInvocation: invocation,
148
+ });
149
+ return {
150
+ ...report,
151
+ route: { ...report.route, target: invocation.targetId },
152
+ };
153
+ }
97
154
  function gateInvocationPromptContext(invocation) {
98
155
  const frontmatter = YAML.stringify({
99
156
  oat_gate_run_id: invocation.runId,
@@ -1165,6 +1222,7 @@ function writeReviewGateExecutionFailure(context, payload) {
1165
1222
  project: payload.project,
1166
1223
  projectResolutionSource: payload.projectResolutionSource,
1167
1224
  gateInvocation: payload.gateInvocation,
1225
+ dispatchReport: payload.dispatchReport,
1168
1226
  ...(payload.corroboration
1169
1227
  ? { corroboration: payload.corroboration }
1170
1228
  : {}),
@@ -1192,6 +1250,7 @@ function writeReviewGateUnexpectedFailure(context, payload) {
1192
1250
  project: payload.project,
1193
1251
  projectResolutionSource: payload.projectResolutionSource,
1194
1252
  gateInvocation: payload.gateInvocation,
1253
+ dispatchReport: payload.dispatchReport,
1195
1254
  message,
1196
1255
  });
1197
1256
  return;
@@ -1210,6 +1269,7 @@ function writeReviewGateArtifactValidationFailure(context, payload) {
1210
1269
  artifactPath: payload.artifactPath,
1211
1270
  generatedAt: payload.generatedAt,
1212
1271
  gateInvocation: payload.gateInvocation,
1272
+ dispatchReport: payload.dispatchReport,
1213
1273
  ...(payload.corroboration
1214
1274
  ? { corroboration: payload.corroboration }
1215
1275
  : {}),
@@ -1248,6 +1308,7 @@ function writeReviewGateTargetingFailure(context, payload) {
1248
1308
  artifactPath: payload.artifactPath,
1249
1309
  generatedAt: payload.generatedAt,
1250
1310
  gateInvocation: payload.gateInvocation,
1311
+ dispatchReport: payload.dispatchReport,
1251
1312
  corroboration,
1252
1313
  receiveEligible: false,
1253
1314
  remediable: false,
@@ -1418,11 +1479,13 @@ async function runReviewGate(prompt, options, context, dependencies) {
1418
1479
  });
1419
1480
  const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
1420
1481
  const gateInvocation = createGateInvocationMetadata(runId, selected);
1482
+ const dispatchReport = buildGateDispatchReport(gateInvocation, options.reviewScope?.trim() || 'gate-review');
1421
1483
  postSelectionContext = {
1422
1484
  project: projectPath,
1423
1485
  projectResolutionSource: reviewProject.source,
1424
1486
  target: selected.id,
1425
1487
  gateInvocation,
1488
+ dispatchReport,
1426
1489
  };
1427
1490
  const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
1428
1491
  const before = await listReviewGateArtifactCandidates({
@@ -1454,6 +1517,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1454
1517
  timedOut: childResult.timedOut ?? false,
1455
1518
  timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
1456
1519
  gateInvocation,
1520
+ dispatchReport,
1457
1521
  });
1458
1522
  process.exitCode = childExitCode;
1459
1523
  return;
@@ -1493,6 +1557,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1493
1557
  generatedAt: diagnosticArtifact?.generatedAt ?? null,
1494
1558
  message,
1495
1559
  gateInvocation,
1560
+ dispatchReport,
1496
1561
  corroboration: initialTargetCorroboration,
1497
1562
  });
1498
1563
  process.exitCode = 1;
@@ -1514,6 +1579,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1514
1579
  ? 'Review artifact is missing oat_project for the explicitly declared project.'
1515
1580
  : 'Review artifact project identity does not match the explicitly declared project.',
1516
1581
  gateInvocation,
1582
+ dispatchReport,
1517
1583
  corroboration: initialTargetCorroboration,
1518
1584
  });
1519
1585
  process.exitCode = 1;
@@ -1531,6 +1597,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1531
1597
  message: 'Review artifact oat_generated_at is missing or is not a valid timestamp.',
1532
1598
  recovery: `Set oat_generated_at to a valid timestamp in ${producedArtifact.path}, then rerun the gate. Invoke oat-project-review-receive only after the gate returns a receive-eligible result.`,
1533
1599
  gateInvocation,
1600
+ dispatchReport,
1534
1601
  corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
1535
1602
  });
1536
1603
  process.exitCode = 1;
@@ -1558,6 +1625,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1558
1625
  message: detail,
1559
1626
  recovery: `The review artifact was created at ${producedArtifact.path} but could not be consumed. Fix the artifact format, then rerun the gate to revalidate it. Invoke oat-project-review-receive only after the gate returns a receive-eligible result; if the only issue is a missing zero-count severity heading, rerun the gate to normalize the same artifact instead of creating a new review version.`,
1560
1627
  gateInvocation,
1628
+ dispatchReport,
1561
1629
  corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
1562
1630
  });
1563
1631
  process.exitCode = 1;
@@ -1581,6 +1649,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1581
1649
  : 'Review artifact invocation metadata does not match the gate-owned configured invocation.',
1582
1650
  recovery: `Copy the exact gate invocation fields from the review prompt into ${producedArtifact.path}, then run oat-project-review-receive only after the artifact validates.`,
1583
1651
  gateInvocation,
1652
+ dispatchReport,
1584
1653
  corroboration,
1585
1654
  });
1586
1655
  process.exitCode = 1;
@@ -1597,6 +1666,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1597
1666
  message: 'Review artifact is missing the required gate invocation marker `oat_review_invocation: gate`.',
1598
1667
  recovery: `Set oat_review_invocation: gate in ${producedArtifact.path}, then run oat-project-review-receive only after the artifact validates.`,
1599
1668
  gateInvocation,
1669
+ dispatchReport,
1600
1670
  corroboration,
1601
1671
  });
1602
1672
  process.exitCode = 1;
@@ -1627,6 +1697,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
1627
1697
  handoff,
1628
1698
  diversity: selected.diversity,
1629
1699
  gateInvocation,
1700
+ dispatchReport,
1630
1701
  corroboration,
1631
1702
  });
1632
1703
  process.exitCode = blocking ? 1 : 0;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch-ceiling/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAY9B,OAAO,EAUL,KAAK,uBAAuB,EAY7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AAezB,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAiC5C,UAAU,2BAA2B;IACnC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AA+0ED,wBAAgB,mCAAmC,CACjD,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CAwFT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch-ceiling/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAUL,KAAK,uBAAuB,EAY7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AAsBzB,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAiC5C,UAAU,2BAA2B;IACnC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AA63ED,wBAAgB,mCAAmC,CACjD,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CAkGT"}
@@ -4,12 +4,14 @@ import { buildCommandContext, } from '../../../app/command-context.js';
4
4
  import { getFrontmatterBlock } from '../../shared/frontmatter.js';
5
5
  import { readGlobalOptions } from '../../shared/shared.utils.js';
6
6
  import { compileDispatchPolicyPreset, } from '../../../config/dispatch-ceiling-preset.js';
7
+ import { normalizeDispatchMatrix } from '../../../config/dispatch-matrix.js';
7
8
  import { getDispatchPolicyChoices, managedDispatchPolicyValueList, renderDispatchPolicyChoicesMarkdown, } from '../../../config/dispatch-policy-options.js';
8
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
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
16
  import { Command, Option } from 'commander';
15
17
  import YAML from 'yaml';
@@ -125,131 +127,64 @@ function invalidProjectPolicyMessage(value) {
125
127
  const actual = typeof value === 'string' ? value : String(value);
126
128
  return `Invalid project dispatch policy "${actual}". Valid managed policies: ${validManagedPolicyList()}. Use mode "inherit" for host defaults.`;
127
129
  }
128
- function normalizeProjectMatrixBareValue(provider, value) {
129
- if (typeof value !== 'string') {
130
- return undefined;
131
- }
132
- const trimmed = value.trim();
133
- if (!trimmed) {
134
- return undefined;
135
- }
136
- const order = providerValueOrder(provider);
137
- if (order && !isValidProviderValue(provider, trimmed)) {
138
- return undefined;
139
- }
140
- return trimmed;
141
- }
142
- function normalizeProjectMatrixRouteTarget(value) {
143
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
144
- return undefined;
145
- }
146
- const record = value;
147
- const target = {};
148
- for (const key of ['harness', 'model', 'effort']) {
149
- const rawValue = record[key];
150
- if (typeof rawValue === 'string' && rawValue.trim()) {
151
- target[key] = rawValue.trim();
152
- }
153
- }
154
- return Object.keys(target).length > 0 ? target : undefined;
130
+ function isProjectMatrixRecord(value) {
131
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
155
132
  }
156
- function normalizeProjectMatrixRoute(provider, value) {
157
- if (!Array.isArray(value) || value.length === 0) {
158
- return undefined;
159
- }
160
- const route = [];
161
- for (const entry of value) {
162
- const bareEntry = normalizeProjectMatrixBareValue(provider, entry);
163
- if (bareEntry !== undefined) {
164
- 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;
165
138
  continue;
166
139
  }
167
- const target = normalizeProjectMatrixRouteTarget(entry);
168
- if (target !== undefined) {
169
- route.push(target);
170
- }
171
- }
172
- return route.length > 0 ? route : undefined;
173
- }
174
- function normalizeProjectMatrixCandidate(provider, value) {
175
- const bareValue = normalizeProjectMatrixBareValue(provider, value);
176
- if (bareValue !== undefined) {
177
- return bareValue;
178
- }
179
- if (isWorkflowDispatchFallbackRoute(value)) {
180
- const route = normalizeProjectMatrixRoute(provider, value.route);
181
- return route ? { route } : undefined;
182
- }
183
- return normalizeProjectMatrixRouteTarget(value);
184
- }
185
- function normalizeProjectMatrixCell(provider, value) {
186
- const bareValue = normalizeProjectMatrixBareValue(provider, value);
187
- if (bareValue !== undefined) {
188
- return bareValue;
189
- }
190
- if (isWorkflowDispatchCandidateLadder(value)) {
191
- const candidates = [];
192
- for (const candidate of value.candidates) {
193
- const normalized = normalizeProjectMatrixCandidate(provider, candidate);
194
- if (normalized !== undefined) {
195
- candidates.push(normalized);
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;
196
152
  }
153
+ if (Array.isArray(rawCell) &&
154
+ firstCandidate !== undefined &&
155
+ isWorkflowDispatchFallbackRoute(firstCandidate)) {
156
+ tiers[matrixTier] = firstCandidate.route;
157
+ continue;
158
+ }
159
+ tiers[matrixTier] = cell;
197
160
  }
198
- return candidates.length > 0 ? { candidates } : undefined;
199
- }
200
- return normalizeProjectMatrixRoute(provider, value);
201
- }
202
- function normalizeProjectMatrixProviderValue(provider, value) {
203
- const bareValue = normalizeProjectMatrixBareValue(provider, value);
204
- if (bareValue !== undefined) {
205
- return bareValue;
206
- }
207
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
208
- return undefined;
209
- }
210
- const tierMap = {};
211
- for (const [tier, rawCell] of Object.entries(value)) {
212
- if (!VALID_DISPATCH_MATRIX_TIERS.includes(tier)) {
213
- continue;
214
- }
215
- const normalized = normalizeProjectMatrixCell(provider, rawCell);
216
- if (normalized !== undefined) {
217
- tierMap[tier] = normalized;
218
- }
161
+ compatibility[provider] = tiers;
219
162
  }
220
- return Object.keys(tierMap).length > 0 ? tierMap : undefined;
163
+ return compatibility;
221
164
  }
222
165
  function readProjectDispatchMatrix(value) {
223
166
  if (value === undefined || value === null) {
224
- return { matrix: null, warnings: [] };
225
- }
226
- if (typeof value !== 'object' || Array.isArray(value)) {
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) {
227
175
  return {
228
176
  matrix: null,
177
+ compatibilityMatrix: null,
229
178
  warnings: [
230
179
  'Ignoring malformed oat_dispatch_policy.matrix in project state.',
231
180
  ],
232
181
  };
233
182
  }
234
- const matrix = {};
235
- for (const [provider, rawProviderValue] of Object.entries(value)) {
236
- if (!provider.trim()) {
237
- continue;
238
- }
239
- const normalized = normalizeProjectMatrixProviderValue(provider, rawProviderValue);
240
- if (normalized !== undefined) {
241
- matrix[provider] = normalized;
242
- }
243
- }
244
- if (Object.keys(matrix).length === 0) {
245
- return {
246
- matrix: null,
247
- warnings: [
248
- 'Ignoring malformed oat_dispatch_policy.matrix in project state.',
249
- ],
250
- };
251
- }
252
- return { matrix, warnings: [] };
183
+ return {
184
+ matrix,
185
+ compatibilityMatrix: toProjectMatrixCompatibility(value, matrix),
186
+ warnings: [],
187
+ };
253
188
  }
254
189
  function policyTier(policy) {
255
190
  return typeof policy === 'string' &&
@@ -585,6 +520,7 @@ function resolveRequestedMatrixCandidate(provider, requested, ceilingTier, resol
585
520
  }
586
521
  allowedMatch ??= {
587
522
  tier,
523
+ candidateIndex,
588
524
  candidate,
589
525
  cellSource: definition.cellSource,
590
526
  routeSignature,
@@ -613,6 +549,7 @@ function resolveRequestedMatrixCandidate(provider, requested, ceilingTier, resol
613
549
  : 'candidate-requested',
614
550
  },
615
551
  candidateTier: allowedMatch.tier,
552
+ candidateIndex: allowedMatch.candidateIndex,
616
553
  };
617
554
  }
618
555
  function readProjectDispatchPolicy(provider, content) {
@@ -639,9 +576,11 @@ function readProjectDispatchPolicy(provider, content) {
639
576
  source: 'project-state',
640
577
  preset: null,
641
578
  matrix: parsedMatrix.matrix,
579
+ matrixCompatibility: parsedMatrix.compatibilityMatrix,
642
580
  cellSource: null,
643
581
  target: null,
644
582
  selectionBranch: 'inherit',
583
+ candidateIndex: null,
645
584
  warnings: parsedMatrix.warnings,
646
585
  };
647
586
  }
@@ -662,9 +601,11 @@ function readProjectDispatchPolicy(provider, content) {
662
601
  source: 'project-state',
663
602
  preset: policyValue,
664
603
  matrix: parsedMatrix.matrix,
604
+ matrixCompatibility: parsedMatrix.compatibilityMatrix,
665
605
  cellSource: null,
666
606
  target: null,
667
607
  selectionBranch: 'prompt-persisted',
608
+ candidateIndex: null,
668
609
  warnings: parsedMatrix.warnings,
669
610
  };
670
611
  }
@@ -689,9 +630,11 @@ function readProjectDispatchPolicy(provider, content) {
689
630
  source: 'project-state',
690
631
  preset: policyValue,
691
632
  matrix: parsedMatrix.matrix,
633
+ matrixCompatibility: parsedMatrix.compatibilityMatrix,
692
634
  cellSource: 'project-state',
693
635
  target: null,
694
636
  selectionBranch: 'prompt-persisted',
637
+ candidateIndex: null,
695
638
  warnings: parsedMatrix.warnings,
696
639
  };
697
640
  }
@@ -722,6 +665,7 @@ function readLegacyProjectDispatchCeiling(provider, parsed) {
722
665
  cellSource: 'project-state',
723
666
  target: null,
724
667
  selectionBranch: 'prompt-persisted',
668
+ candidateIndex: null,
725
669
  warnings: [],
726
670
  };
727
671
  }
@@ -790,9 +734,11 @@ function stripConfigCandidateSource(candidate) {
790
734
  source: candidate.source,
791
735
  preset: candidate.preset,
792
736
  matrix: candidate.matrix,
737
+ matrixCompatibility: candidate.matrixCompatibility,
793
738
  cellSource: candidate.cellSource,
794
739
  target: candidate.target,
795
740
  selectionBranch: candidate.selectionBranch,
741
+ candidateIndex: candidate.candidateIndex,
796
742
  warnings: candidate.warnings,
797
743
  };
798
744
  }
@@ -815,6 +761,7 @@ function readResolvedConfigPolicyCandidate(provider, resolvedConfig) {
815
761
  cellSource: null,
816
762
  target: null,
817
763
  selectionBranch: 'inherit',
764
+ candidateIndex: null,
818
765
  warnings: [],
819
766
  configSource: modeEntry.source,
820
767
  };
@@ -840,6 +787,7 @@ function readResolvedConfigPolicyCandidate(provider, resolvedConfig) {
840
787
  cellSource: null,
841
788
  target: null,
842
789
  selectionBranch: 'prompt-persisted',
790
+ candidateIndex: null,
843
791
  warnings: [],
844
792
  configSource,
845
793
  };
@@ -870,6 +818,7 @@ function readResolvedLegacyConfigCeilingCandidate(provider, resolvedConfig) {
870
818
  cellSource: source,
871
819
  target: null,
872
820
  selectionBranch: 'prompt-persisted',
821
+ candidateIndex: null,
873
822
  warnings: [],
874
823
  configSource: entry.source,
875
824
  };
@@ -998,6 +947,7 @@ function selectDispatchValue(provider, role, policy, preferredValue) {
998
947
  target: policy.target,
999
948
  requestedCandidate: policy.requestedCandidate ?? null,
1000
949
  candidateTier: policy.candidateTier ?? null,
950
+ candidateIndex: policy.candidateIndex,
1001
951
  ceilingTier: policy.ceilingTier ?? policyTier(policy.policy),
1002
952
  ceilingTarget: policy.ceilingTarget ?? policy.target,
1003
953
  };
@@ -1160,6 +1110,7 @@ function buildProviderResolution(provider, policy, role, orchestratorTier, prefe
1160
1110
  preferredValue,
1161
1111
  requestedCandidate: null,
1162
1112
  candidateTier: null,
1113
+ candidateIndex: null,
1163
1114
  ceilingTier: null,
1164
1115
  ceilingTarget: null,
1165
1116
  selectedValue: null,
@@ -1294,7 +1245,7 @@ async function resolveDispatchCeiling(context, dependencies, options) {
1294
1245
  unresolved: true,
1295
1246
  projectPath,
1296
1247
  providerDefaultEffort,
1297
- matrix: resolvedValue.matrix,
1248
+ matrix: resolvedValue.matrixCompatibility ?? resolvedValue.matrix,
1298
1249
  providers,
1299
1250
  message,
1300
1251
  };
@@ -1310,7 +1261,7 @@ async function resolveDispatchCeiling(context, dependencies, options) {
1310
1261
  unresolved: false,
1311
1262
  projectPath,
1312
1263
  providerDefaultEffort,
1313
- matrix: resolvedValue.matrix,
1264
+ matrix: resolvedValue.matrixCompatibility ?? resolvedValue.matrix,
1314
1265
  providers,
1315
1266
  };
1316
1267
  }
@@ -1356,6 +1307,7 @@ async function resolveCeilingValue(provider, resolvedConfig, projectPath, depend
1356
1307
  source: 'invocation',
1357
1308
  preset: invocationCeilingTier,
1358
1309
  matrix: projectCeiling?.matrix ?? baseCeiling.matrix,
1310
+ matrixCompatibility: projectCeiling?.matrixCompatibility ?? baseCeiling.matrixCompatibility,
1359
1311
  cellSource: null,
1360
1312
  target: null,
1361
1313
  selectionBranch: 'prompt-persisted',
@@ -1391,6 +1343,7 @@ async function resolveCeilingValue(provider, resolvedConfig, projectPath, depend
1391
1343
  ],
1392
1344
  requestedCandidate,
1393
1345
  candidateTier: selected.candidateTier,
1346
+ candidateIndex: selected.candidateIndex,
1394
1347
  ceilingTarget: ceilingCell?.target ?? null,
1395
1348
  ...(ceilingTier ? { ceilingTier } : {}),
1396
1349
  };
@@ -1533,14 +1486,91 @@ function writeHumanResolution(context, resolution) {
1533
1486
  }
1534
1487
  }
1535
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
+ }
1536
1562
  async function runDispatchCeilingResolve(context, dependencies, options) {
1537
1563
  try {
1538
1564
  const resolution = await resolveDispatchCeiling(context, dependencies, options);
1565
+ const dispatchReport = buildResolutionReport(resolution, options);
1539
1566
  if (context.json) {
1540
- context.logger.json(resolution);
1567
+ context.logger.json(dispatchReport ? { ...resolution, dispatchReport } : resolution);
1541
1568
  }
1542
1569
  else {
1543
1570
  writeHumanResolution(context, resolution);
1571
+ if (dispatchReport) {
1572
+ context.logger.info(formatDispatchReport(dispatchReport));
1573
+ }
1544
1574
  }
1545
1575
  process.exitCode = resolution.status === 'blocked' ? 1 : 0;
1546
1576
  }
@@ -1602,6 +1632,8 @@ export function createProjectDispatchCeilingCommand(overrides = {}) {
1602
1632
  .option('--project-path <path>', 'Read project-state policy from an explicit project path')
1603
1633
  .option('--preflight', 'Treat unresolved non-interactive resolution as an implementation block')
1604
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']))
1605
1637
  .option('--json', 'Output machine-readable JSON')
1606
1638
  .action(async (options, cmd) => {
1607
1639
  const globalOptions = readGlobalOptions(cmd);
@@ -0,0 +1,59 @@
1
+ export type WorkflowDispatchMatrixTier = 'economy' | 'balanced' | 'high' | 'frontier';
2
+ export interface WorkflowDispatchRouteTarget {
3
+ harness?: string;
4
+ model?: string;
5
+ effort?: string;
6
+ }
7
+ export interface DispatchRouteTargetValidation {
8
+ valid: boolean;
9
+ reason?: string;
10
+ }
11
+ export type WorkflowDispatchRouteEntry = string | WorkflowDispatchRouteTarget;
12
+ export type WorkflowDispatchRoute = WorkflowDispatchRouteEntry[];
13
+ export interface WorkflowDispatchFallbackRoute {
14
+ route: WorkflowDispatchRoute;
15
+ }
16
+ export type WorkflowDispatchCandidate = WorkflowDispatchRouteEntry | WorkflowDispatchFallbackRoute;
17
+ export interface WorkflowDispatchCandidateLadder {
18
+ candidates: WorkflowDispatchCandidate[];
19
+ }
20
+ export type WorkflowDispatchLegacyMatrixCell = string | WorkflowDispatchRoute;
21
+ export type WorkflowDispatchMatrixCell = WorkflowDispatchCandidateLadder | WorkflowDispatchLegacyMatrixCell;
22
+ export type WorkflowDispatchProviderValue = string | Partial<Record<WorkflowDispatchMatrixTier, WorkflowDispatchMatrixCell>>;
23
+ export type DispatchMatrixNormalizationIssueKind = 'malformed-provider' | 'malformed-tier' | 'malformed-candidate';
24
+ export interface DispatchMatrixNormalizationIssue {
25
+ path: string;
26
+ kind: DispatchMatrixNormalizationIssueKind;
27
+ value: unknown;
28
+ }
29
+ export interface NormalizedDispatchMatrix {
30
+ providers: Record<string, WorkflowDispatchProviderValue>;
31
+ issues: DispatchMatrixNormalizationIssue[];
32
+ }
33
+ export type DispatchMatrixSource = 'local-config' | 'repo-config' | 'user-config' | 'project-state';
34
+ export interface DispatchMatrixWalkContext {
35
+ source: DispatchMatrixSource;
36
+ pathPrefix: string;
37
+ }
38
+ export interface DispatchMatrixCellRef {
39
+ provider: string;
40
+ tier: WorkflowDispatchMatrixTier | null;
41
+ candidateIndex: number | null;
42
+ fallbackRouteIndex: number | null;
43
+ value: string | null;
44
+ target: WorkflowDispatchRouteTarget | null;
45
+ path: string;
46
+ source: DispatchMatrixSource;
47
+ }
48
+ export declare const VALID_DISPATCH_MATRIX_TIERS: readonly WorkflowDispatchMatrixTier[];
49
+ export declare function normalizeDispatchMatrix(value: unknown, options: {
50
+ pathPrefix: string;
51
+ compatibilityMode: 'layered-config' | 'project-state';
52
+ }): NormalizedDispatchMatrix;
53
+ export declare function isCodexMaterializedRouteTarget(provider: string, target: WorkflowDispatchRouteTarget): boolean;
54
+ export declare function validateDispatchRouteTarget(provider: string, target: WorkflowDispatchRouteTarget): DispatchRouteTargetValidation;
55
+ export declare function isWorkflowDispatchFallbackRoute(value: unknown): value is WorkflowDispatchFallbackRoute;
56
+ export declare function isWorkflowDispatchCandidateLadder(value: unknown): value is WorkflowDispatchCandidateLadder;
57
+ export declare function toWorkflowDispatchCandidateLadder(cell: WorkflowDispatchMatrixCell): WorkflowDispatchCandidateLadder;
58
+ export declare function walkDispatchMatrix(providers: Record<string, WorkflowDispatchProviderValue>, context: DispatchMatrixWalkContext): DispatchMatrixCellRef[];
59
+ //# sourceMappingURL=dispatch-matrix.d.ts.map