@open-agent-toolkit/cli 0.1.45 → 0.1.46

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 (69) hide show
  1. package/assets/agents/oat-reviewer.md +2 -2
  2. package/assets/docs/cli-utilities/config-and-local-state.md +7 -0
  3. package/assets/docs/cli-utilities/configuration.md +51 -11
  4. package/assets/docs/provider-sync/config.md +5 -1
  5. package/assets/docs/provider-sync/manifest-and-drift.md +6 -1
  6. package/assets/docs/provider-sync/providers.md +6 -3
  7. package/assets/docs/provider-sync/scope-and-surface.md +2 -1
  8. package/assets/docs/reference/oat-directory-structure.md +2 -2
  9. package/assets/docs/workflows/projects/dispatch-ceiling.md +41 -28
  10. package/assets/docs/workflows/projects/implementation-execution.md +20 -13
  11. package/assets/public-package-versions.json +4 -4
  12. package/assets/skills/oat-project-implement/SKILL.md +114 -59
  13. package/assets/skills/oat-project-quick-start/SKILL.md +32 -20
  14. package/dist/commands/config/index.d.ts +2 -2
  15. package/dist/commands/config/index.d.ts.map +1 -1
  16. package/dist/commands/config/index.js +71 -20
  17. package/dist/commands/doctor/index.d.ts +2 -2
  18. package/dist/commands/doctor/index.d.ts.map +1 -1
  19. package/dist/commands/doctor/index.js +83 -27
  20. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  21. package/dist/commands/project/dispatch-ceiling/index.js +137 -10
  22. package/dist/commands/providers/codex/index.d.ts +4 -0
  23. package/dist/commands/providers/codex/index.d.ts.map +1 -0
  24. package/dist/commands/providers/codex/index.js +7 -0
  25. package/dist/commands/providers/codex/materialize.d.ts +5 -0
  26. package/dist/commands/providers/codex/materialize.d.ts.map +1 -0
  27. package/dist/commands/providers/codex/materialize.js +152 -0
  28. package/dist/commands/providers/index.d.ts +2 -2
  29. package/dist/commands/providers/index.d.ts.map +1 -1
  30. package/dist/commands/providers/index.js +4 -2
  31. package/dist/commands/providers/providers.types.d.ts +23 -0
  32. package/dist/commands/providers/providers.types.d.ts.map +1 -1
  33. package/dist/commands/shared/codex-strays.d.ts.map +1 -1
  34. package/dist/commands/shared/codex-strays.js +11 -1
  35. package/dist/commands/status/index.d.ts +4 -1
  36. package/dist/commands/status/index.d.ts.map +1 -1
  37. package/dist/commands/status/index.js +1 -1
  38. package/dist/commands/sync/index.d.ts.map +1 -1
  39. package/dist/commands/sync/index.js +1 -1
  40. package/dist/commands/sync/sync.types.d.ts +4 -1
  41. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  42. package/dist/config/dispatch-ceiling-preset.d.ts +4 -0
  43. package/dist/config/dispatch-ceiling-preset.d.ts.map +1 -1
  44. package/dist/config/dispatch-ceiling-preset.js +3 -0
  45. package/dist/config/dispatch-policy-options.d.ts +20 -0
  46. package/dist/config/dispatch-policy-options.d.ts.map +1 -0
  47. package/dist/config/dispatch-policy-options.js +96 -0
  48. package/dist/config/oat-config.d.ts +6 -0
  49. package/dist/config/oat-config.d.ts.map +1 -1
  50. package/dist/config/oat-config.js +15 -0
  51. package/dist/providers/ceiling/registry.d.ts +7 -5
  52. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  53. package/dist/providers/ceiling/registry.js +16 -5
  54. package/dist/providers/codex/codec/config-merge.d.ts +6 -0
  55. package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
  56. package/dist/providers/codex/codec/config-merge.js +7 -0
  57. package/dist/providers/codex/codec/materialize.d.ts +16 -0
  58. package/dist/providers/codex/codec/materialize.d.ts.map +1 -0
  59. package/dist/providers/codex/codec/materialize.js +57 -0
  60. package/dist/providers/codex/codec/shared.d.ts +1 -0
  61. package/dist/providers/codex/codec/shared.d.ts.map +1 -1
  62. package/dist/providers/codex/codec/shared.js +9 -1
  63. package/dist/providers/codex/codec/sync-extension.d.ts +7 -1
  64. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  65. package/dist/providers/codex/codec/sync-extension.js +183 -46
  66. package/dist/providers/identity/availability.d.ts +24 -1
  67. package/dist/providers/identity/availability.d.ts.map +1 -1
  68. package/dist/providers/identity/availability.js +253 -19
  69. package/package.json +2 -2
@@ -1,14 +1,17 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { readFile, rm, writeFile } from 'node:fs/promises';
3
- import { dirname, join, relative, resolve } from 'node:path';
2
+ import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join, relative, resolve } from 'node:path';
4
4
  import { parseCanonicalAgentFile } from '../../../agents/canonical/index.js';
5
+ import { isCodexMaterializedRouteTarget, resolveActiveProject, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
6
+ import { resolveEffectiveConfig } from '../../../config/resolve.js';
5
7
  import { ensureDir, fileExists } from '../../../fs/io.js';
6
8
  import TOML from '@iarna/toml';
9
+ import YAML from 'yaml';
7
10
  import { mergeCodexConfig } from './config-merge.js';
8
11
  import { exportCanonicalAgentToCodexRole } from './export-to-codex.js';
12
+ import { materializeCodexRole } from './materialize.js';
9
13
  import { isOatManagedCodexRoleFile } from './shared.js';
10
- const CODEX_EFFORT_VARIANTS = ['low', 'medium', 'high', 'xhigh'];
11
- const CODEX_EFFORT_VARIANT_BASE_ROLES = new Set([
14
+ const CODEX_MATERIALIZED_BASE_ROLES = new Set([
12
15
  'oat-phase-implementer',
13
16
  'oat-reviewer',
14
17
  ]);
@@ -42,41 +45,7 @@ function normalizeManagedRolesConfig(desiredRoles) {
42
45
  configFile: role.configFile,
43
46
  }));
44
47
  }
45
- function codexEffortVariantContent(baseContent, baseRoleName, variantRoleName, effort) {
46
- return baseContent
47
- .replace(`# oat-role: ${baseRoleName}`, `# oat-role: ${variantRoleName}`)
48
- .replace('developer_instructions =', `model_reasoning_effort = "${effort}"\ndeveloper_instructions =`);
49
- }
50
- function codexEffortVariantDescription(baseRoleName, effort) {
51
- const roleLabel = baseRoleName === 'oat-reviewer' ? 'reviewer' : 'phase implementer';
52
- if (effort === 'low') {
53
- return `OAT ${roleLabel} pinned to low reasoning effort for narrow mechanical implementation, fix, or review phases.`;
54
- }
55
- if (effort === 'medium') {
56
- return `OAT ${roleLabel} pinned to medium reasoning effort for normal multi-file implementation, fix, or review phases.`;
57
- }
58
- if (effort === 'high') {
59
- return `OAT ${roleLabel} pinned to high reasoning effort for broad, subtle, or higher-risk implementation, fix, or review phases.`;
60
- }
61
- return `OAT ${roleLabel} pinned to xhigh reasoning effort for the highest configured Codex dispatch policy cap.`;
62
- }
63
- function codexEffortVariantsFromBase(exported) {
64
- if (!CODEX_EFFORT_VARIANT_BASE_ROLES.has(exported.roleName)) {
65
- return [];
66
- }
67
- return CODEX_EFFORT_VARIANTS.map((effort) => {
68
- const variantRoleName = `${exported.roleName}-${effort}`;
69
- const configFile = `agents/${variantRoleName}.toml`;
70
- return {
71
- roleName: variantRoleName,
72
- description: codexEffortVariantDescription(exported.roleName, effort),
73
- configFile,
74
- rolePath: join(dirname(exported.rolePath), `${variantRoleName}.toml`),
75
- content: codexEffortVariantContent(exported.content, exported.roleName, variantRoleName, effort),
76
- };
77
- });
78
- }
79
- async function desiredRolesFromCanonical(canonicalEntries, scopeRoot) {
48
+ async function desiredRolesFromCanonical(canonicalEntries, scopeRoot, materializationTargets) {
80
49
  const roles = [];
81
50
  for (const entry of canonicalEntries) {
82
51
  if (entry.type !== 'agent' ||
@@ -93,10 +62,154 @@ async function desiredRolesFromCanonical(canonicalEntries, scopeRoot) {
93
62
  rolePath: join(scopeRoot, '.codex', exported.configFile),
94
63
  content: exported.content,
95
64
  });
96
- roles.push(...codexEffortVariantsFromBase(roles[roles.length - 1]));
65
+ if (CODEX_MATERIALIZED_BASE_ROLES.has(exported.roleName)) {
66
+ for (const target of materializationTargets) {
67
+ const materialized = materializeCodexRole({
68
+ agent: parsed,
69
+ model: target.model,
70
+ effort: target.effort,
71
+ });
72
+ roles.push({
73
+ roleName: materialized.roleName,
74
+ description: materialized.description,
75
+ configFile: materialized.configFile,
76
+ rolePath: join(scopeRoot, '.codex', materialized.configFile),
77
+ content: materialized.content,
78
+ });
79
+ }
80
+ }
97
81
  }
98
82
  return roles.sort((left, right) => left.roleName.localeCompare(right.roleName));
99
83
  }
84
+ function collectCodexTargetFromEntry(entry, targets) {
85
+ if (typeof entry === 'string') {
86
+ return;
87
+ }
88
+ if (!isCodexMaterializedRouteTarget('codex', entry)) {
89
+ return;
90
+ }
91
+ const validation = validateDispatchRouteTarget('codex', entry);
92
+ if (!validation.valid || !entry.model || !entry.effort) {
93
+ return;
94
+ }
95
+ targets.set(`${entry.model}\0${entry.effort}`, {
96
+ model: entry.model,
97
+ effort: entry.effort,
98
+ });
99
+ }
100
+ function collectCodexTargetsFromCell(cell, targets) {
101
+ if (typeof cell === 'string') {
102
+ return;
103
+ }
104
+ for (const entry of cell) {
105
+ collectCodexTargetFromEntry(entry, targets);
106
+ }
107
+ }
108
+ function collectCodexMaterializationTargetsFromProvider(providerValue, targets) {
109
+ if (providerValue === undefined || typeof providerValue === 'string') {
110
+ return;
111
+ }
112
+ for (const cell of Object.values(providerValue)) {
113
+ if (cell !== undefined) {
114
+ collectCodexTargetsFromCell(cell, targets);
115
+ }
116
+ }
117
+ }
118
+ function sortCodexMaterializationTargets(targets) {
119
+ return [...targets.values()].sort((left, right) => {
120
+ const modelOrder = left.model.localeCompare(right.model);
121
+ return modelOrder === 0
122
+ ? left.effort.localeCompare(right.effort)
123
+ : modelOrder;
124
+ });
125
+ }
126
+ function routeTargetFromUnknown(value) {
127
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
128
+ return null;
129
+ }
130
+ const record = value;
131
+ const target = {};
132
+ for (const key of ['harness', 'model', 'effort']) {
133
+ const rawValue = record[key];
134
+ if (typeof rawValue === 'string' && rawValue.trim()) {
135
+ target[key] = rawValue.trim();
136
+ }
137
+ }
138
+ return Object.keys(target).length > 0 ? target : null;
139
+ }
140
+ function collectCodexTargetsFromUnknownCell(value, targets) {
141
+ if (typeof value === 'string' || !Array.isArray(value)) {
142
+ return;
143
+ }
144
+ for (const entry of value) {
145
+ if (typeof entry === 'string') {
146
+ continue;
147
+ }
148
+ const target = routeTargetFromUnknown(entry);
149
+ if (target) {
150
+ collectCodexTargetFromEntry(target, targets);
151
+ }
152
+ }
153
+ }
154
+ function collectCodexTargetsFromUnknownProvider(value, targets) {
155
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
156
+ return;
157
+ }
158
+ for (const cell of Object.values(value)) {
159
+ collectCodexTargetsFromUnknownCell(cell, targets);
160
+ }
161
+ }
162
+ function frontmatterBlock(content) {
163
+ const normalized = content.startsWith('\uFEFF') ? content.slice(1) : content;
164
+ if (!normalized.startsWith('---\n')) {
165
+ return null;
166
+ }
167
+ const end = normalized.indexOf('\n---', 4);
168
+ return end > 0 ? normalized.slice(4, end) : null;
169
+ }
170
+ async function collectProjectStateCodexTargets(scopeRoot, options, targets) {
171
+ const projectPath = options.projectPath === undefined
172
+ ? (await resolveActiveProject(scopeRoot)).path
173
+ : options.projectPath;
174
+ if (!projectPath) {
175
+ return;
176
+ }
177
+ const statePath = join(resolve(scopeRoot, projectPath), 'state.md');
178
+ const stateContent = await readOptionalFile(statePath);
179
+ if (!stateContent) {
180
+ return;
181
+ }
182
+ const frontmatter = frontmatterBlock(stateContent);
183
+ if (!frontmatter) {
184
+ return;
185
+ }
186
+ const parsed = YAML.parse(frontmatter);
187
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
188
+ return;
189
+ }
190
+ const policy = parsed['oat_dispatch_policy'];
191
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
192
+ return;
193
+ }
194
+ const matrix = policy['matrix'];
195
+ if (!matrix || typeof matrix !== 'object' || Array.isArray(matrix)) {
196
+ return;
197
+ }
198
+ collectCodexTargetsFromUnknownProvider(matrix['codex'], targets);
199
+ }
200
+ async function readCodexMaterializationTargets(scopeRoot, options = {}) {
201
+ const targets = new Map();
202
+ const effectiveConfig = await resolveEffectiveConfig(scopeRoot, options.userConfigDir ?? join(scopeRoot, '.oat', '__no-user-config__'), options.env);
203
+ for (const providerValue of [
204
+ effectiveConfig.user.workflow?.dispatchCeiling?.providers?.codex,
205
+ effectiveConfig.shared.workflow?.dispatchCeiling?.providers?.codex,
206
+ effectiveConfig.local.workflow?.dispatchCeiling?.providers?.codex,
207
+ ]) {
208
+ collectCodexMaterializationTargetsFromProvider(providerValue, targets);
209
+ }
210
+ await collectProjectStateCodexTargets(scopeRoot, options, targets);
211
+ return sortCodexMaterializationTargets(targets);
212
+ }
100
213
  function parseConfigAgentTable(content) {
101
214
  if (!content || content.trim() === '') {
102
215
  return {};
@@ -124,7 +237,7 @@ function parseConfigAgentTable(content) {
124
237
  }
125
238
  async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desiredRoleNames) {
126
239
  const agents = parseConfigAgentTable(existingConfigContent);
127
- const stale = [];
240
+ const stale = new Set();
128
241
  for (const [roleName, roleConfig] of Object.entries(agents)) {
129
242
  if (desiredRoleNames.has(roleName)) {
130
243
  continue;
@@ -138,13 +251,37 @@ async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desire
138
251
  if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
139
252
  continue;
140
253
  }
141
- stale.push(roleName);
254
+ stale.add(roleName);
255
+ }
256
+ const agentsDir = join(scopeRoot, '.codex', 'agents');
257
+ let roleFiles;
258
+ try {
259
+ roleFiles = await readdir(agentsDir);
260
+ }
261
+ catch {
262
+ roleFiles = [];
263
+ }
264
+ for (const roleFile of roleFiles) {
265
+ if (!roleFile.endsWith('.toml')) {
266
+ continue;
267
+ }
268
+ const roleName = basename(roleFile, '.toml');
269
+ if (desiredRoleNames.has(roleName) || stale.has(roleName)) {
270
+ continue;
271
+ }
272
+ const rolePath = join(agentsDir, roleFile);
273
+ const roleContent = await readOptionalFile(rolePath);
274
+ if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
275
+ continue;
276
+ }
277
+ stale.add(roleName);
142
278
  }
143
- return stale.sort((left, right) => left.localeCompare(right));
279
+ return [...stale].sort((left, right) => left.localeCompare(right));
144
280
  }
145
- export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntries, allowedCanonicalPaths) {
281
+ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntries, allowedCanonicalPaths, options = {}) {
146
282
  const isPartialSync = allowedCanonicalPaths !== undefined && allowedCanonicalPaths.length > 0;
147
- const desiredRoles = await desiredRolesFromCanonical(canonicalEntries.filter((entry) => canonicalPathAllowed(scopeRoot, entry, allowedCanonicalPaths)), scopeRoot);
283
+ const materializationTargets = await readCodexMaterializationTargets(scopeRoot, options);
284
+ const desiredRoles = await desiredRolesFromCanonical(canonicalEntries.filter((entry) => canonicalPathAllowed(scopeRoot, entry, allowedCanonicalPaths)), scopeRoot, materializationTargets);
148
285
  const desiredRoleNames = new Set(desiredRoles.map((role) => role.roleName));
149
286
  const existingConfigPath = configPath(scopeRoot);
150
287
  const existingConfigContent = await readOptionalFile(existingConfigPath);
@@ -227,7 +364,7 @@ export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntri
227
364
  });
228
365
  return {
229
366
  operations,
230
- managedRoles: desiredRoles.map((role) => role.roleName),
367
+ managedRoles: [...desiredRoles.map((role) => role.roleName), ...staleRoles],
231
368
  aggregateConfigHash: hashContent(configMerge.mergedContent),
232
369
  };
233
370
  }
@@ -1,4 +1,10 @@
1
1
  export type MatrixCellAvailability = 'valid' | 'unknown-value' | 'unvalidated';
2
+ export interface MatrixCellAvailabilityResult {
3
+ availability: MatrixCellAvailability;
4
+ allowedValues?: string[];
5
+ message?: string;
6
+ }
7
+ export type MatrixCellAvailabilityResponse = MatrixCellAvailability | MatrixCellAvailabilityResult;
2
8
  export interface CursorAgentRunOptions {
3
9
  cwd: string;
4
10
  env: NodeJS.ProcessEnv;
@@ -8,15 +14,32 @@ export interface CursorAgentRunResult {
8
14
  stdout: string;
9
15
  stderr: string;
10
16
  }
17
+ export interface CodexRunOptions {
18
+ cwd: string;
19
+ env: NodeJS.ProcessEnv;
20
+ }
21
+ export interface CodexRunResult {
22
+ ok: boolean;
23
+ stdout: string;
24
+ stderr: string;
25
+ }
11
26
  export interface AvailabilityOracleDependencies {
12
27
  pathExists: (path: string) => Promise<boolean>;
13
28
  runCursorAgent: (args: string[], options: CursorAgentRunOptions) => Promise<CursorAgentRunResult>;
29
+ runCodex: (args: string[], options: CodexRunOptions) => Promise<CodexRunResult>;
14
30
  env?: NodeJS.ProcessEnv;
15
31
  }
16
32
  export interface ValidateMatrixCellOptions {
17
33
  cwd: string;
34
+ detailed?: boolean;
18
35
  env?: NodeJS.ProcessEnv;
19
36
  dependencies?: Partial<AvailabilityOracleDependencies>;
37
+ target?: {
38
+ model?: string;
39
+ effort?: string;
40
+ } | null;
20
41
  }
21
- export declare function validateMatrixCell(provider: string, value: string, options: ValidateMatrixCellOptions): Promise<MatrixCellAvailability>;
42
+ export declare function normalizeMatrixCellAvailability(result: MatrixCellAvailabilityResponse): MatrixCellAvailabilityResult;
43
+ export declare function validateCursorSubagentModel(value: string, options: ValidateMatrixCellOptions): Promise<MatrixCellAvailabilityResult>;
44
+ export declare function validateMatrixCell(provider: string, value: string, options: ValidateMatrixCellOptions): Promise<MatrixCellAvailabilityResponse>;
22
45
  //# sourceMappingURL=availability.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"availability.d.ts","sourceRoot":"","sources":["../../../src/providers/identity/availability.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,eAAe,GAAG,aAAa,CAAC;AAE/E,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,cAAc,EAAE,CACd,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,qBAAqB,KAC3B,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;CACxD;AAwID,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,sBAAsB,CAAC,CA8BjC"}
1
+ {"version":3,"file":"availability.d.ts","sourceRoot":"","sources":["../../../src/providers/identity/availability.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,eAAe,GAAG,aAAa,CAAC;AAE/E,MAAM,WAAW,4BAA4B;IAC3C,YAAY,EAAE,sBAAsB,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,8BAA8B,GACtC,sBAAsB,GACtB,4BAA4B,CAAC;AAEjC,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,8BAA8B;IAC7C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,cAAc,EAAE,CACd,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,qBAAqB,KAC3B,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,eAAe,KACrB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,IAAI,CAAC;CACV;AAWD,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,8BAA8B,GACrC,4BAA4B,CAE9B;AA+VD,wBAAsB,2BAA2B,CAC/C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,4BAA4B,CAAC,CAsEvC;AAED,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,8BAA8B,CAAC,CA2CzC"}
@@ -1,9 +1,11 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { join } from 'node:path';
3
2
  import { promisify } from 'node:util';
4
- import { VALID_CLAUDE_DISPATCH_CEILINGS, VALID_CODEX_DISPATCH_CEILINGS, } from '../../config/oat-config.js';
3
+ import { VALID_CLAUDE_DISPATCH_CEILINGS } from '../../config/oat-config.js';
5
4
  import { fileExists } from '../../fs/io.js';
6
5
  const execFileAsync = promisify(execFile);
6
+ export function normalizeMatrixCellAvailability(result) {
7
+ return typeof result === 'string' ? { availability: result } : result;
8
+ }
7
9
  async function runCursorAgent(args, options) {
8
10
  try {
9
11
  const { stdout, stderr } = await execFileAsync('cursor-agent', args, {
@@ -27,9 +29,33 @@ async function runCursorAgent(args, options) {
27
29
  };
28
30
  }
29
31
  }
32
+ async function runCodex(args, options) {
33
+ try {
34
+ const { stdout, stderr } = await execFileAsync('codex', args, {
35
+ cwd: options.cwd,
36
+ env: options.env,
37
+ maxBuffer: 16 * 1024 * 1024,
38
+ timeout: 10_000,
39
+ });
40
+ return {
41
+ ok: true,
42
+ stdout: String(stdout),
43
+ stderr: String(stderr),
44
+ };
45
+ }
46
+ catch (error) {
47
+ const failure = error;
48
+ return {
49
+ ok: false,
50
+ stdout: String(failure.stdout ?? ''),
51
+ stderr: String(failure.stderr ?? failure.message),
52
+ };
53
+ }
54
+ }
30
55
  const DEFAULT_DEPENDENCIES = {
31
56
  pathExists: fileExists,
32
57
  runCursorAgent,
58
+ runCodex,
33
59
  env: process.env,
34
60
  };
35
61
  function apiKeyArgs(env) {
@@ -61,43 +87,245 @@ function availabilityFromCursorCatalog(value, stdout) {
61
87
  ? 'valid'
62
88
  : 'unknown-value';
63
89
  }
64
- async function validateCodexCell(value, cwd, dependencies) {
65
- if (!VALID_CODEX_DISPATCH_CEILINGS.includes(value)) {
66
- return 'unknown-value';
90
+ const CURSOR_SUBAGENT_PROBE_SENTINEL = 'OAT_CURSOR_SUBAGENT_MODEL_VALID';
91
+ function hasCursorSubagentProbeSentinel(stdout) {
92
+ return stdout
93
+ .split(/\r?\n/)
94
+ .some((line) => line.trim() === CURSOR_SUBAGENT_PROBE_SENTINEL);
95
+ }
96
+ function cursorSubagentProbePrompt(model) {
97
+ return [
98
+ 'Validate whether a Cursor subagent Task can be launched with a specific model.',
99
+ `Use the Task tool once with model "${model}" and ask the subagent to reply exactly: ${CURSOR_SUBAGENT_PROBE_SENTINEL}.`,
100
+ 'After the subagent returns, print only its exact reply.',
101
+ ].join('\n');
102
+ }
103
+ function cursorSubagentProbeArgs(model, env) {
104
+ return [
105
+ ...apiKeyArgs(env),
106
+ '-p',
107
+ cursorSubagentProbePrompt(model),
108
+ '--output-format=text',
109
+ '--force',
110
+ ];
111
+ }
112
+ function unique(values) {
113
+ return [...new Set(values)];
114
+ }
115
+ function extractModelSlugs(value) {
116
+ const matches = value.match(/\b[a-z][a-z0-9]*(?:[-.][a-z0-9]+)+\b/gi) ?? [];
117
+ return unique(matches
118
+ .map((match) => match.replace(/[),.;:]+$/g, ''))
119
+ .filter((match) => /\d/.test(match)));
120
+ }
121
+ function parseCursorAllowedSubagentModels(output) {
122
+ const sections = [];
123
+ const patterns = [
124
+ /\ballowed\s+(?:subagent\s+)?models?\s*:?\s*([^\n]+)/gi,
125
+ /\bsupported\s+(?:subagent\s+)?models?\s*:?\s*([^\n]+)/gi,
126
+ /\bvalid\s+(?:subagent\s+)?models?\s*:?\s*([^\n]+)/gi,
127
+ /\bone\s+of\s*:?\s*([^\n]+)/gi,
128
+ ];
129
+ for (const pattern of patterns) {
130
+ for (const match of output.matchAll(pattern)) {
131
+ if (match[1]) {
132
+ sections.push(match[1]);
133
+ }
134
+ }
135
+ }
136
+ return unique(sections.flatMap(extractModelSlugs));
137
+ }
138
+ function cursorAllowedModelsMessage(allowedValues) {
139
+ return `Allowed subagent models: ${allowedValues.join(', ')}.`;
140
+ }
141
+ function cursorRejectedMessage(allowedValues) {
142
+ return `Cursor rejected this model for subagent Task dispatch. ${cursorAllowedModelsMessage(allowedValues)}`;
143
+ }
144
+ function cursorCatalogContextMessage(value, availability) {
145
+ if (availability === 'valid') {
146
+ return `Cursor's broad model catalog lists '${value}', but subagent Task dispatch could not be validated.`;
147
+ }
148
+ if (availability === 'unknown-value') {
149
+ return `Cursor's broad model catalog does not list '${value}'.`;
150
+ }
151
+ return undefined;
152
+ }
153
+ function isRecord(value) {
154
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
155
+ }
156
+ function stringFromUnknown(value) {
157
+ return typeof value === 'string' && value.trim().length > 0
158
+ ? value.trim()
159
+ : null;
160
+ }
161
+ function extractCodexEffort(value) {
162
+ if (typeof value === 'string') {
163
+ return stringFromUnknown(value);
164
+ }
165
+ if (!isRecord(value)) {
166
+ return null;
167
+ }
168
+ return (stringFromUnknown(value.effort) ??
169
+ stringFromUnknown(value.id) ??
170
+ stringFromUnknown(value.name));
171
+ }
172
+ function extractCodexEfforts(model) {
173
+ const efforts = [];
174
+ for (const key of [
175
+ 'supported_reasoning_levels',
176
+ 'supported_reasoning_efforts',
177
+ 'supported_efforts',
178
+ ]) {
179
+ const value = model[key];
180
+ if (!Array.isArray(value)) {
181
+ continue;
182
+ }
183
+ efforts.push(...value
184
+ .map(extractCodexEffort)
185
+ .filter((effort) => effort !== null));
67
186
  }
187
+ const defaultEffort = stringFromUnknown(model.default_reasoning_level);
188
+ if (defaultEffort) {
189
+ efforts.push(defaultEffort);
190
+ }
191
+ return unique(efforts);
192
+ }
193
+ function parseCodexCatalog(stdout) {
194
+ let parsed;
68
195
  try {
69
- const implementerExists = await dependencies.pathExists(join(cwd, '.codex', 'agents', `oat-phase-implementer-${value}.toml`));
70
- const reviewerExists = await dependencies.pathExists(join(cwd, '.codex', 'agents', `oat-reviewer-${value}.toml`));
71
- return implementerExists && reviewerExists ? 'valid' : 'unknown-value';
196
+ parsed = JSON.parse(stdout);
72
197
  }
73
198
  catch {
74
- return 'unvalidated';
199
+ return null;
200
+ }
201
+ if (!isRecord(parsed) || !Array.isArray(parsed.models)) {
202
+ return null;
203
+ }
204
+ const entries = [];
205
+ for (const model of parsed.models) {
206
+ if (!isRecord(model)) {
207
+ continue;
208
+ }
209
+ const slug = stringFromUnknown(model.slug);
210
+ if (!slug) {
211
+ continue;
212
+ }
213
+ const supportedEfforts = extractCodexEfforts(model);
214
+ entries.push({
215
+ slug,
216
+ supportedEfforts: supportedEfforts.length > 0 ? supportedEfforts : null,
217
+ });
218
+ }
219
+ return entries.length > 0 ? entries : null;
220
+ }
221
+ function codexTargetFromValue(value, target) {
222
+ const targetModel = stringFromUnknown(target?.model);
223
+ const targetEffort = stringFromUnknown(target?.effort);
224
+ if (targetModel && targetEffort) {
225
+ return { model: targetModel, effort: targetEffort };
226
+ }
227
+ const [model, effort] = value.split('/');
228
+ return {
229
+ model: stringFromUnknown(model),
230
+ effort: stringFromUnknown(effort),
231
+ };
232
+ }
233
+ function codexUnsupportedEffortMessage(model, effort, supportedEfforts) {
234
+ return `Codex debug models lists '${model}', but effort '${effort}' is not supported. Supported Codex efforts: ${supportedEfforts.join(', ')}.`;
235
+ }
236
+ async function validateCodexCell(value, cwd, dependencies, target) {
237
+ const { model, effort } = codexTargetFromValue(value, target);
238
+ if (!model || !effort || value.trim().length === 0) {
239
+ return { availability: 'unknown-value' };
240
+ }
241
+ const result = await dependencies.runCodex(['debug', 'models'], {
242
+ cwd,
243
+ env: dependencies.env ?? process.env,
244
+ });
245
+ if (!result.ok) {
246
+ return { availability: 'unvalidated' };
247
+ }
248
+ const catalog = parseCodexCatalog(result.stdout);
249
+ if (!catalog) {
250
+ return { availability: 'unvalidated' };
251
+ }
252
+ const entry = catalog.find((modelEntry) => modelEntry.slug === model);
253
+ if (!entry) {
254
+ return {
255
+ availability: 'unknown-value',
256
+ message: `Codex debug models does not list '${model}'.`,
257
+ };
258
+ }
259
+ if (!entry.supportedEfforts) {
260
+ return {
261
+ availability: 'unvalidated',
262
+ message: `Codex debug models lists '${model}', but supported reasoning efforts could not be parsed.`,
263
+ };
75
264
  }
265
+ if (!entry.supportedEfforts.includes(effort)) {
266
+ return {
267
+ availability: 'unknown-value',
268
+ allowedValues: entry.supportedEfforts,
269
+ message: codexUnsupportedEffortMessage(model, effort, entry.supportedEfforts),
270
+ };
271
+ }
272
+ return { availability: 'valid' };
76
273
  }
77
- async function validateCursorCell(value, cwd, dependencies) {
274
+ export async function validateCursorSubagentModel(value, options) {
275
+ const dependencies = {
276
+ ...DEFAULT_DEPENDENCIES,
277
+ env: options.env ?? DEFAULT_DEPENDENCIES.env,
278
+ ...options.dependencies,
279
+ };
78
280
  const env = dependencies.env ?? process.env;
79
- const runOptions = { cwd, env };
281
+ const runOptions = { cwd: options.cwd, env };
282
+ const probeResult = await dependencies.runCursorAgent(cursorSubagentProbeArgs(value, env), runOptions);
283
+ const probeOutput = `${probeResult.stdout}\n${probeResult.stderr}`;
284
+ if (probeResult.ok && hasCursorSubagentProbeSentinel(probeResult.stdout)) {
285
+ return { availability: 'valid' };
286
+ }
287
+ const allowedValues = parseCursorAllowedSubagentModels(probeOutput);
288
+ if (allowedValues.length > 0) {
289
+ const availability = allowedValues.includes(value)
290
+ ? 'valid'
291
+ : 'unknown-value';
292
+ return {
293
+ availability,
294
+ allowedValues,
295
+ message: availability === 'valid'
296
+ ? cursorAllowedModelsMessage(allowedValues)
297
+ : cursorRejectedMessage(allowedValues),
298
+ };
299
+ }
80
300
  const modelsResult = await dependencies.runCursorAgent([...apiKeyArgs(env), 'models'], runOptions);
81
301
  if (modelsResult.ok) {
82
302
  const availability = availabilityFromCursorCatalog(value, modelsResult.stdout);
83
303
  if (availability !== null) {
84
- return availability;
304
+ return {
305
+ availability: availability === 'valid' ? 'unvalidated' : 'unknown-value',
306
+ message: cursorCatalogContextMessage(value, availability),
307
+ };
85
308
  }
86
309
  }
87
310
  const listResult = await dependencies.runCursorAgent([...apiKeyArgs(env), '--list-models'], runOptions);
88
311
  if (listResult.ok) {
89
312
  const availability = availabilityFromCursorCatalog(value, listResult.stdout);
90
313
  if (availability !== null) {
91
- return availability;
314
+ return {
315
+ availability: availability === 'valid' ? 'unvalidated' : 'unknown-value',
316
+ message: cursorCatalogContextMessage(value, availability),
317
+ };
92
318
  }
93
319
  }
94
- return 'unvalidated';
320
+ return { availability: 'unvalidated' };
95
321
  }
96
322
  export async function validateMatrixCell(provider, value, options) {
97
323
  const normalizedProvider = provider.trim().toLowerCase();
98
324
  const normalizedValue = value.trim();
99
325
  if (!normalizedProvider || !normalizedValue) {
100
- return 'unknown-value';
326
+ return options.detailed
327
+ ? { availability: 'unknown-value' }
328
+ : 'unknown-value';
101
329
  }
102
330
  const dependencies = {
103
331
  ...DEFAULT_DEPENDENCIES,
@@ -105,15 +333,21 @@ export async function validateMatrixCell(provider, value, options) {
105
333
  ...options.dependencies,
106
334
  };
107
335
  if (normalizedProvider === 'claude') {
108
- return VALID_CLAUDE_DISPATCH_CEILINGS.includes(normalizedValue)
336
+ const availability = VALID_CLAUDE_DISPATCH_CEILINGS.includes(normalizedValue)
109
337
  ? 'valid'
110
338
  : 'unknown-value';
339
+ return options.detailed ? { availability } : availability;
111
340
  }
112
341
  if (normalizedProvider === 'codex') {
113
- return validateCodexCell(normalizedValue, options.cwd, dependencies);
342
+ const result = await validateCodexCell(normalizedValue, options.cwd, dependencies, options.target);
343
+ return options.detailed ? result : result.availability;
114
344
  }
115
345
  if (normalizedProvider === 'cursor') {
116
- return validateCursorCell(normalizedValue, options.cwd, dependencies);
346
+ const result = await validateCursorSubagentModel(normalizedValue, {
347
+ ...options,
348
+ dependencies,
349
+ });
350
+ return options.detailed ? result : result.availability;
117
351
  }
118
- return 'unvalidated';
352
+ return options.detailed ? { availability: 'unvalidated' } : 'unvalidated';
119
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.0.0",
35
35
  "yaml": "2.8.2",
36
36
  "zod": "^3.25.76",
37
- "@open-agent-toolkit/control-plane": "0.1.45"
37
+ "@open-agent-toolkit/control-plane": "0.1.46"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",