@open-agent-toolkit/cli 0.1.39 → 0.1.41

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/backlog-lifecycle.md +61 -0
  2. package/assets/docs/cli-utilities/config-and-local-state.md +47 -0
  3. package/assets/docs/cli-utilities/configuration.md +2 -0
  4. package/assets/docs/cli-utilities/index.md +1 -0
  5. package/assets/docs/provider-sync/instruction-sync.md +1 -0
  6. package/assets/docs/workflows/projects/dispatch-ceiling.md +14 -3
  7. package/assets/docs/workflows/projects/implementation-execution.md +16 -2
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/oat-pjm-review-backlog/SKILL.md +29 -7
  10. package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +25 -5
  11. package/assets/skills/oat-project-implement/SKILL.md +41 -10
  12. package/assets/templates/pjm-agents.md +80 -1
  13. package/assets/templates/pjm-handoffs-readme.md +21 -0
  14. package/assets/templates/reference-agents.md +13 -0
  15. package/assets/templates/repo-agents.md +4 -0
  16. package/assets/templates/repo-readme.md +35 -0
  17. package/dist/commands/backlog/archive.d.ts +33 -0
  18. package/dist/commands/backlog/archive.d.ts.map +1 -0
  19. package/dist/commands/backlog/archive.js +229 -0
  20. package/dist/commands/backlog/index.d.ts +2 -0
  21. package/dist/commands/backlog/index.d.ts.map +1 -1
  22. package/dist/commands/backlog/index.js +56 -2
  23. package/dist/commands/backlog/regenerate-index.d.ts +5 -1
  24. package/dist/commands/backlog/regenerate-index.d.ts.map +1 -1
  25. package/dist/commands/backlog/regenerate-index.js +7 -1
  26. package/dist/commands/backlog/shared/item-status.d.ts +13 -0
  27. package/dist/commands/backlog/shared/item-status.d.ts.map +1 -0
  28. package/dist/commands/backlog/shared/item-status.js +34 -0
  29. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  30. package/dist/commands/instructions/instructions.utils.js +24 -0
  31. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  32. package/dist/commands/pjm/doctor.js +161 -0
  33. package/dist/commands/pjm/index.d.ts.map +1 -1
  34. package/dist/commands/pjm/index.js +2 -1
  35. package/dist/commands/pjm/init.d.ts +2 -1
  36. package/dist/commands/pjm/init.d.ts.map +1 -1
  37. package/dist/commands/pjm/init.js +7 -0
  38. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  39. package/dist/commands/project/dispatch-ceiling/index.js +80 -4
  40. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  import { access, readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { BACKLOG_ITEM_STATUSES, extractBacklogStatus, isTerminalBacklogStatus, isValidBacklogStatus, } from '../backlog/shared/item-status.js';
3
4
  import { getFrontmatterBlock } from '../shared/frontmatter.js';
4
5
  import { CANONICAL_REPO_REFERENCE_PATHS } from './init.js';
5
6
  const ALLOWED_TOP_LEVEL_DIRECTORIES = new Set([
@@ -108,6 +109,45 @@ function containsTemplateFrontmatter(content) {
108
109
  (/\boat_template\s*:/i.test(frontmatter) ||
109
110
  /\boat_template_name\s*:/i.test(frontmatter)));
110
111
  }
112
+ const BACKLOG_ITEMS_DIRECTORY = 'pjm/backlog/items';
113
+ const BACKLOG_ARCHIVED_DIRECTORY = 'pjm/backlog/archived';
114
+ const BACKLOG_COMPLETED_FILE = 'pjm/backlog/completed.md';
115
+ // Best-effort backlog id scan: `BL-YYMMDD-slug` (legacy lowercase `bl-`
116
+ // matched case-insensitively). The six-digit requirement keeps the literal
117
+ // `BL-YYMMDD-slug` format placeholder in `completed.md` from matching.
118
+ const BACKLOG_ID_PATTERN = /\bbl-\d{6}-[a-z0-9][a-z0-9-]*/gi;
119
+ /**
120
+ * Single frontmatter pass over both backlog directories. Every drift check
121
+ * derives from this scan so the filesystem is walked once. `index.md` is the
122
+ * managed index, not an item, so it is excluded.
123
+ */
124
+ async function collectBacklogItems(repoRoot) {
125
+ const records = [];
126
+ const directories = [
127
+ { directory: 'items', relativeDir: BACKLOG_ITEMS_DIRECTORY },
128
+ { directory: 'archived', relativeDir: BACKLOG_ARCHIVED_DIRECTORY },
129
+ ];
130
+ for (const { directory, relativeDir } of directories) {
131
+ const fileNames = await listMarkdownFiles(join(repoRoot, relativeDir));
132
+ for (const fileName of fileNames) {
133
+ if (fileName === 'index.md') {
134
+ continue;
135
+ }
136
+ const content = await readIfExists(join(repoRoot, relativeDir, fileName));
137
+ records.push({
138
+ directory,
139
+ relativePath: `${relativeDir}/${fileName}`,
140
+ id: fileName.replace(/\.md$/, ''),
141
+ status: content ? extractBacklogStatus(content) : null,
142
+ });
143
+ }
144
+ }
145
+ return records;
146
+ }
147
+ /** Best-effort scan of completed-log content for backlog item ids. */
148
+ function extractCompletedIds(content) {
149
+ return [...content.matchAll(BACKLOG_ID_PATTERN)].map((match) => match[0]);
150
+ }
111
151
  function checkStatus(missing) {
112
152
  return missing.length === 0 ? 'pass' : 'fail';
113
153
  }
@@ -246,5 +286,126 @@ export async function runPjmDoctorChecks(repoRoot, options = {}) {
246
286
  ? undefined
247
287
  : 'Move active operational docs to pjm/ and leave reference/ for durable append-mostly artifacts.',
248
288
  });
289
+ const backlogItems = await collectBacklogItems(repoRoot);
290
+ const terminalInItems = backlogItems
291
+ .filter((item) => item.directory === 'items' &&
292
+ item.status !== null &&
293
+ isTerminalBacklogStatus(item.status))
294
+ .map((item) => item.relativePath);
295
+ checks.push({
296
+ name: 'pjm:backlog_terminal_in_items',
297
+ description: 'Terminal-status backlog items awaiting archive',
298
+ status: checkStatus(terminalInItems),
299
+ message: terminalInItems.length === 0
300
+ ? 'No terminal-status backlog items remain under items/.'
301
+ : `Terminal-status backlog items still under items/: ${terminalInItems.join(', ')}`,
302
+ fix: terminalInItems.length === 0
303
+ ? undefined
304
+ : 'Run `oat backlog archive <id>` to move each closed/wont_do item into archived/.',
305
+ });
306
+ // A missing/empty `status:` is as much a drift as an out-of-enum value: the
307
+ // `oat backlog archive` command rejects both, so `pjm doctor` must surface
308
+ // both rather than leaving status-less items silently invisible.
309
+ const missingStatusItems = backlogItems
310
+ .filter((item) => item.status === null || item.status.trim() === '')
311
+ .map((item) => item.relativePath);
312
+ const outOfEnumStatusItems = backlogItems
313
+ .filter((item) => item.status !== null &&
314
+ item.status.trim() !== '' &&
315
+ !isValidBacklogStatus(item.status))
316
+ .map((item) => item.relativePath);
317
+ const invalidStatus = [...outOfEnumStatusItems, ...missingStatusItems];
318
+ const invalidStatusDetails = [];
319
+ if (outOfEnumStatusItems.length > 0) {
320
+ invalidStatusDetails.push(`out-of-enum status: ${outOfEnumStatusItems.join(', ')}`);
321
+ }
322
+ if (missingStatusItems.length > 0) {
323
+ invalidStatusDetails.push(`missing status: ${missingStatusItems.join(', ')}`);
324
+ }
325
+ checks.push({
326
+ name: 'pjm:backlog_invalid_status',
327
+ description: 'Backlog items with a missing or out-of-enum status',
328
+ status: checkStatus(invalidStatus),
329
+ message: invalidStatus.length === 0
330
+ ? 'All backlog item statuses are within the valid enum.'
331
+ : `Backlog items with an invalid status (${invalidStatusDetails.join('; ')}). Valid statuses: ${BACKLOG_ITEM_STATUSES.join(', ')}.`,
332
+ fix: invalidStatus.length === 0
333
+ ? undefined
334
+ : `Set a valid status (${BACKLOG_ITEM_STATUSES.join(', ')}) on each listed item.`,
335
+ });
336
+ const archivedOpen = backlogItems
337
+ .filter((item) => item.directory === 'archived' &&
338
+ item.status !== null &&
339
+ isValidBacklogStatus(item.status) &&
340
+ !isTerminalBacklogStatus(item.status))
341
+ .map((item) => item.relativePath);
342
+ checks.push({
343
+ name: 'pjm:backlog_archived_open',
344
+ description: 'Archived backlog items with a non-terminal status',
345
+ status: warnStatus(archivedOpen),
346
+ message: archivedOpen.length === 0
347
+ ? 'No archived backlog items carry an open status.'
348
+ : `Archived backlog items still marked open/in_progress: ${archivedOpen.join(', ')}`,
349
+ fix: archivedOpen.length === 0
350
+ ? undefined
351
+ : 'Set a terminal status (closed/wont_do) on each archived item, or move it back under items/.',
352
+ });
353
+ // A single id must never live in both `items/` and `archived/`: the live copy
354
+ // shadows the archived record and `oat backlog archive` refuses to reconcile
355
+ // it automatically (auto-archiving would clobber the archived file). Derive
356
+ // from the same single scan so the filesystem is not re-walked.
357
+ const backlogPathsByIdAndDir = new Map();
358
+ for (const item of backlogItems) {
359
+ const key = item.id.toLowerCase();
360
+ const entry = backlogPathsByIdAndDir.get(key) ?? {};
361
+ entry[item.directory] = item.relativePath;
362
+ backlogPathsByIdAndDir.set(key, entry);
363
+ }
364
+ const duplicateIdPairs = [];
365
+ for (const entry of backlogPathsByIdAndDir.values()) {
366
+ if (entry.items && entry.archived) {
367
+ duplicateIdPairs.push(`${entry.items} + ${entry.archived}`);
368
+ }
369
+ }
370
+ checks.push({
371
+ name: 'pjm:backlog_duplicate_id',
372
+ description: 'Backlog ids present in both items/ and archived/',
373
+ status: checkStatus(duplicateIdPairs),
374
+ message: duplicateIdPairs.length === 0
375
+ ? 'No backlog id is present in both items/ and archived/.'
376
+ : `Backlog ids present in both items/ and archived/: ${duplicateIdPairs.join('; ')}`,
377
+ fix: duplicateIdPairs.length === 0
378
+ ? undefined
379
+ : 'Reconcile each duplicate manually: decide whether the active (items/) or archived copy is authoritative, remove the other, then re-run `oat backlog archive <id>` if the item still needs archiving.',
380
+ });
381
+ const itemPathsById = new Map();
382
+ for (const item of backlogItems) {
383
+ if (item.directory === 'items') {
384
+ itemPathsById.set(item.id.toLowerCase(), item.relativePath);
385
+ }
386
+ }
387
+ const completedContent = await readIfExists(join(repoRoot, BACKLOG_COMPLETED_FILE));
388
+ const completedUnarchived = [];
389
+ const seenUnarchived = new Set();
390
+ if (completedContent) {
391
+ for (const completedId of extractCompletedIds(completedContent)) {
392
+ const relativePath = itemPathsById.get(completedId.toLowerCase());
393
+ if (relativePath && !seenUnarchived.has(relativePath)) {
394
+ seenUnarchived.add(relativePath);
395
+ completedUnarchived.push(relativePath);
396
+ }
397
+ }
398
+ }
399
+ checks.push({
400
+ name: 'pjm:backlog_completed_unarchived',
401
+ description: 'Completed-log entries whose item file is still under items/',
402
+ status: warnStatus(completedUnarchived),
403
+ message: completedUnarchived.length === 0
404
+ ? 'No completed backlog entries reference an item still under items/.'
405
+ : `Completed log references items still under items/: ${completedUnarchived.join(', ')}`,
406
+ fix: completedUnarchived.length === 0
407
+ ? undefined
408
+ : 'Run `oat backlog archive <id>` to archive each completed item still under items/.',
409
+ });
249
410
  return checks;
250
411
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAiBnE,UAAU,sBAAsB;IAC9B,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,cAAc,EAAE,OAAO,cAAc,CAAC;IACtC,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;CACvD;AA6FD,wBAAgB,gBAAgB,CAC9B,SAAS,GAAE,OAAO,CAAC,sBAAsB,CAAM,GAC9C,OAAO,CAsJT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAA0B,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAiBnE,UAAU,sBAAsB;IAC9B,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,cAAc,EAAE,OAAO,cAAc,CAAC;IACtC,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;CACvD;AA6FD,wBAAgB,gBAAgB,CAC9B,SAAS,GAAE,OAAO,CAAC,sBAAsB,CAAM,GAC9C,OAAO,CAuJT"}
@@ -7,7 +7,7 @@ import { resolveProjectRoot } from '../../fs/paths.js';
7
7
  import { formatDoctorResults } from '../../ui/output.js';
8
8
  import { Command } from 'commander';
9
9
  import { runPjmDoctorChecks } from './doctor.js';
10
- import { initializeRepoReference } from './init.js';
10
+ import { initializeRepoReference, INSTRUCTIONS_SYNC_HINT } from './init.js';
11
11
  import { migratePjmRepo, readPjmMigrationPrompt } from './migrate.js';
12
12
  const DEFAULT_DEPENDENCIES = {
13
13
  buildCommandContext,
@@ -111,6 +111,7 @@ export function createPjmCommand(overrides = {}) {
111
111
  if (result.skipped.length > 0) {
112
112
  context.logger.info(`Skipped existing: ${result.skipped.join(', ')}`);
113
113
  }
114
+ context.logger.info(INSTRUCTIONS_SYNC_HINT);
114
115
  }
115
116
  process.exitCode = 0;
116
117
  }
@@ -8,6 +8,7 @@ export interface RepoReferenceInitResult {
8
8
  created: string[];
9
9
  skipped: string[];
10
10
  }
11
- export declare const CANONICAL_REPO_REFERENCE_PATHS: readonly [...("AGENTS.md" | "pjm/AGENTS.md" | "pjm/current-state.md" | "pjm/roadmap.md" | "reference/AGENTS.md")[], "pjm/backlog/index.md", "pjm/backlog/completed.md", "pjm/backlog/items/.gitkeep", "pjm/backlog/archived/.gitkeep", "reference/decisions/index.md"];
11
+ export declare const INSTRUCTIONS_SYNC_HINT: string;
12
+ export declare const CANONICAL_REPO_REFERENCE_PATHS: readonly [...("AGENTS.md" | "pjm/AGENTS.md" | "pjm/current-state.md" | "pjm/roadmap.md" | "reference/AGENTS.md" | "README.md" | "pjm/handoffs/README.md")[], "pjm/backlog/index.md", "pjm/backlog/completed.md", "pjm/backlog/items/.gitkeep", "pjm/backlog/archived/.gitkeep", "reference/decisions/index.md"];
12
13
  export declare function initializeRepoReference(options: InitializeRepoReferenceOptions): Promise<RepoReferenceInitResult>;
13
14
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAwBD,eAAO,MAAM,8BAA8B,wQAIjC,CAAC;AAyEX,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CAgElC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAoBD,eAAO,MAAM,sBAAsB,QAEiD,CAAC;AAWrF,eAAO,MAAM,8BAA8B,iTAIjC,CAAC;AAyEX,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CAgElC"}
@@ -9,7 +9,14 @@ const TEMPLATE_TARGETS = [
9
9
  { template: 'current-state.md', target: 'pjm/current-state.md' },
10
10
  { template: 'roadmap.md', target: 'pjm/roadmap.md' },
11
11
  { template: 'reference-agents.md', target: 'reference/AGENTS.md' },
12
+ { template: 'repo-readme.md', target: 'README.md' },
13
+ { template: 'pjm-handoffs-readme.md', target: 'pjm/handoffs/README.md' },
12
14
  ];
15
+ // Next-step hint printed after init/backfill. `oat pjm init` never writes
16
+ // CLAUDE.md shims itself — strategy ownership stays with `oat instructions
17
+ // sync`, so we point the operator at it (with the `--dry-run` preview).
18
+ export const INSTRUCTIONS_SYNC_HINT = 'Next step: run `oat instructions sync` to create CLAUDE.md shims for the ' +
19
+ 'repo-reference AGENTS.md files (preview with `oat instructions sync --dry-run`).';
13
20
  const BACKLOG_PATHS = [
14
21
  'pjm/backlog/index.md',
15
22
  'pjm/backlog/completed.md',
@@ -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;AAG9B,OAAO,EAEL,KAAK,uBAAuB,EAG7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AASzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC,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;AAggBD,wBAAgB,mCAAmC,CACjD,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CAiDT"}
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;AAG9B,OAAO,EAEL,KAAK,uBAAuB,EAG7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AASzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6BpC,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;AAknBD,wBAAgB,mCAAmC,CACjD,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CAqDT"}
@@ -173,12 +173,67 @@ function readResolvedConfigCeiling(provider, resolvedConfig) {
173
173
  function normalizeRole(value) {
174
174
  return value === 'reviewer' ? 'reviewer' : 'implementer';
175
175
  }
176
+ function providerValueOrder(provider) {
177
+ if (provider === 'codex') {
178
+ return CODEX_VALUES;
179
+ }
180
+ if (provider === 'claude') {
181
+ return CLAUDE_VALUES;
182
+ }
183
+ return null;
184
+ }
185
+ function normalizePreferredValue(provider, value) {
186
+ if (!value) {
187
+ return null;
188
+ }
189
+ const normalized = value.trim();
190
+ if (!normalized) {
191
+ return null;
192
+ }
193
+ const order = providerValueOrder(provider);
194
+ if (!order) {
195
+ return null;
196
+ }
197
+ if (!isValidProviderValue(provider, normalized)) {
198
+ const validValues = order.join(', ');
199
+ throw new Error(`Invalid preferred dispatch value "${normalized}" for ${provider}. Valid values: ${validValues}.`);
200
+ }
201
+ return normalized;
202
+ }
203
+ function selectDispatchValue(provider, role, ceilingValue, preferredValue) {
204
+ if (role === 'reviewer' || preferredValue === null) {
205
+ return {
206
+ role,
207
+ preferredValue,
208
+ selectedValue: ceilingValue,
209
+ capped: false,
210
+ };
211
+ }
212
+ const order = providerValueOrder(provider);
213
+ const preferredIndex = order?.indexOf(preferredValue) ?? -1;
214
+ const ceilingIndex = order?.indexOf(ceilingValue) ?? -1;
215
+ if (!order || preferredIndex < 0 || ceilingIndex < 0) {
216
+ return {
217
+ role,
218
+ preferredValue,
219
+ selectedValue: ceilingValue,
220
+ capped: false,
221
+ };
222
+ }
223
+ const selectedIndex = Math.min(preferredIndex, ceilingIndex);
224
+ return {
225
+ role,
226
+ preferredValue,
227
+ selectedValue: order[selectedIndex],
228
+ capped: preferredIndex > ceilingIndex,
229
+ };
230
+ }
176
231
  /**
177
232
  * Join a resolved ceiling value with the active provider's adapter to compute
178
233
  * the enforcement mode, mechanism, dispatch args, and verify-on-upgrade flag.
179
234
  * Mode is computed here at call time — it is never read from persisted state.
180
235
  */
181
- function buildProviderResolution(provider, value, role, orchestratorTier) {
236
+ function buildProviderResolution(provider, value, role, orchestratorTier, preferredValue) {
182
237
  const adapter = getCeilingAdapter(provider);
183
238
  if (value === null) {
184
239
  return {
@@ -187,9 +242,17 @@ function buildProviderResolution(provider, value, role, orchestratorTier) {
187
242
  mechanism: adapter.mechanism,
188
243
  dispatchArgs: null,
189
244
  verifyOnDispatch: false,
245
+ selection: {
246
+ role,
247
+ preferredValue,
248
+ selectedValue: null,
249
+ capped: false,
250
+ },
190
251
  };
191
252
  }
192
- const dispatchArgs = adapter.compileToDispatchArgs(value, role, {
253
+ const selection = selectDispatchValue(provider, role, value, preferredValue);
254
+ const dispatchValue = selection.selectedValue ?? value;
255
+ const dispatchArgs = adapter.compileToDispatchArgs(dispatchValue, role, {
193
256
  orchestratorTier,
194
257
  });
195
258
  let mode;
@@ -207,7 +270,10 @@ function buildProviderResolution(provider, value, role, orchestratorTier) {
207
270
  mode,
208
271
  mechanism: adapter.mechanism,
209
272
  dispatchArgs,
210
- verifyOnDispatch: adapter.verifyOnDispatch(value, { orchestratorTier }),
273
+ verifyOnDispatch: adapter.verifyOnDispatch(dispatchValue, {
274
+ orchestratorTier,
275
+ }),
276
+ selection,
211
277
  };
212
278
  }
213
279
  function readCodexDefaultFromToml(content) {
@@ -257,8 +323,9 @@ async function resolveDispatchCeiling(context, dependencies, options) {
257
323
  const providerDefaultEffort = provider === 'codex'
258
324
  ? await resolveCodexProviderDefaultEffort(repoRoot, context, dependencies)
259
325
  : 'not-applicable';
326
+ const preferredValue = normalizePreferredValue(provider, options.preferred);
260
327
  const resolvedValue = await resolveCeilingValue(provider, resolvedConfig, projectPath, dependencies);
261
- const providerResolution = buildProviderResolution(provider, resolvedValue?.value ?? null, role, orchestratorTier);
328
+ const providerResolution = buildProviderResolution(provider, resolvedValue?.value ?? null, role, orchestratorTier, preferredValue);
262
329
  const providers = {
263
330
  [provider]: providerResolution,
264
331
  };
@@ -332,9 +399,17 @@ function writeHumanResolution(context, resolution) {
332
399
  if (resolution.provider === 'codex') {
333
400
  context.logger.info(`Codex provider default effort: ${resolution.providerDefaultEffort}`);
334
401
  context.logger.info(`Note: OAT will use pinned subagent variants up to ${resolution.value ?? 'the resolved ceiling'}. Base/unpinned roles resolve through the provider default.`);
402
+ if (providerResolution?.selection.selectedValue &&
403
+ providerResolution.selection.preferredValue) {
404
+ context.logger.info(`Selected Codex effort: ${providerResolution.selection.selectedValue}`);
405
+ }
335
406
  }
336
407
  else {
337
408
  context.logger.info('Effort axis: not-applicable');
409
+ if (providerResolution?.selection.selectedValue &&
410
+ providerResolution.selection.preferredValue) {
411
+ context.logger.info(`Selected dispatch value: ${providerResolution.selection.selectedValue}`);
412
+ }
338
413
  }
339
414
  }
340
415
  async function runDispatchCeilingResolve(context, dependencies, options) {
@@ -370,6 +445,7 @@ export function createProjectDispatchCeilingCommand(overrides = {}) {
370
445
  .requiredOption('--provider <provider>', 'Provider name: codex or claude are enforced; any other provider resolves as advisory (unsupported)')
371
446
  .option('--role <role>', 'Dispatch role for variant compilation: implementer (default) or reviewer')
372
447
  .option('--orchestrator-tier <tier>', 'Orchestrator tier, used to flag verify-on-upgrade for above-orchestrator requests')
448
+ .option('--preferred <value>', 'Preferred implementer/fix dispatch value before capping by the resolved ceiling')
373
449
  .option('--project-path <path>', 'Read project-state ceiling from an explicit project path')
374
450
  .option('--preflight', 'Treat unresolved non-interactive resolution as an implementation block')
375
451
  .option('--non-interactive', 'Force non-interactive block behavior when the ceiling is unresolved')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
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.39"
37
+ "@open-agent-toolkit/control-plane": "0.1.41"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",