@kbediako/codex-orchestrator 0.1.32 → 0.1.34

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 (41) hide show
  1. package/README.md +96 -12
  2. package/codex.orchestrator.json +448 -0
  3. package/dist/bin/codex-orchestrator.js +703 -136
  4. package/dist/orchestrator/src/cli/codexCliSetup.js +1 -0
  5. package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +22 -0
  6. package/dist/orchestrator/src/cli/config/userConfig.js +20 -9
  7. package/dist/orchestrator/src/cli/delegationSetup.js +111 -14
  8. package/dist/orchestrator/src/cli/doctor.js +264 -8
  9. package/dist/orchestrator/src/cli/doctorIssueLog.js +350 -0
  10. package/dist/orchestrator/src/cli/doctorUsage.js +150 -8
  11. package/dist/orchestrator/src/cli/init.js +24 -1
  12. package/dist/orchestrator/src/cli/mcpEnable.js +392 -0
  13. package/dist/orchestrator/src/cli/orchestrator.js +180 -5
  14. package/dist/orchestrator/src/cli/rlmRunner.js +289 -35
  15. package/dist/orchestrator/src/cli/run/manifest.js +31 -6
  16. package/dist/orchestrator/src/cli/services/commandRunner.js +10 -2
  17. package/dist/orchestrator/src/cli/services/pipelineResolver.js +70 -18
  18. package/dist/orchestrator/src/cli/services/runPreparation.js +2 -0
  19. package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
  20. package/dist/orchestrator/src/cli/skills.js +3 -8
  21. package/dist/orchestrator/src/cli/utils/advancedAutopilot.js +114 -0
  22. package/dist/orchestrator/src/cli/utils/codexCli.js +21 -0
  23. package/dist/orchestrator/src/cli/utils/commandPreview.js +10 -0
  24. package/dist/orchestrator/src/cli/utils/delegationGuardRunner.js +85 -8
  25. package/dist/orchestrator/src/cli/utils/devtools.js +2 -1
  26. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +79 -19
  27. package/dist/orchestrator/src/cloud/CodexCloudTaskExecutor.js +46 -6
  28. package/dist/orchestrator/src/control-plane/request-builder.js +9 -8
  29. package/dist/scripts/lib/pr-watch-merge.js +367 -3
  30. package/docs/README.md +17 -11
  31. package/package.json +2 -1
  32. package/schemas/manifest.json +27 -0
  33. package/skills/collab-deliberation/SKILL.md +6 -0
  34. package/skills/collab-evals/SKILL.md +4 -0
  35. package/skills/collab-subagents-first/SKILL.md +29 -7
  36. package/skills/delegation-usage/DELEGATION_GUIDE.md +31 -5
  37. package/skills/delegation-usage/SKILL.md +29 -4
  38. package/skills/elegance-review/SKILL.md +14 -3
  39. package/skills/standalone-review/SKILL.md +8 -2
  40. package/templates/README.md +1 -1
  41. package/templates/codex/AGENTS.md +12 -1
@@ -33,7 +33,16 @@ const DEFAULT_MAX_CONCURRENCY = 4;
33
33
  const DEFAULT_SYMBOLIC_DELIBERATION_INTERVAL = 2;
34
34
  const DEFAULT_SYMBOLIC_DELIBERATION_MAX_RUNS = 12;
35
35
  const DEFAULT_SYMBOLIC_DELIBERATION_MAX_SUMMARY_BYTES = 2048;
36
+ const DEFAULT_COLLAB_ROLE_POLICY = 'enforce';
37
+ const COLLAB_ROLE_POLICY_ENV_CANONICAL = 'RLM_SYMBOLIC_MULTI_AGENT_ROLE_POLICY';
38
+ const COLLAB_ROLE_POLICY_ENV_LEGACY = 'RLM_COLLAB_ROLE_POLICY';
39
+ const COLLAB_ALLOW_DEFAULT_ROLE_ENV_CANONICAL = 'RLM_SYMBOLIC_MULTI_AGENT_ALLOW_DEFAULT_ROLE';
40
+ const COLLAB_ALLOW_DEFAULT_ROLE_ENV_LEGACY = 'RLM_COLLAB_ALLOW_DEFAULT_ROLE';
36
41
  const UNBOUNDED_ITERATION_ALIASES = new Set(['unbounded', 'unlimited', 'infinite', 'infinity']);
42
+ const COLLAB_FEATURE_CANONICAL = 'multi_agent';
43
+ const COLLAB_FEATURE_LEGACY = 'collab';
44
+ const COLLAB_ROLE_TAG_PATTERN = /^\s*\[(?:agent_type|role)\s*:\s*([a-z0-9._-]+)\]/i;
45
+ const COLLAB_ROLE_TOKEN_PATTERN = /^[a-z0-9._-]+$/;
37
46
  function parseArgs(argv) {
38
47
  const parsed = {};
39
48
  for (let i = 0; i < argv.length; i += 1) {
@@ -102,6 +111,15 @@ function envFlagEnabled(value) {
102
111
  const normalized = value.trim().toLowerCase();
103
112
  return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
104
113
  }
114
+ function resolveSymbolicMultiAgentConfig(env) {
115
+ if (env.RLM_SYMBOLIC_MULTI_AGENT !== undefined) {
116
+ return { enabled: envFlagEnabled(env.RLM_SYMBOLIC_MULTI_AGENT), source: 'canonical' };
117
+ }
118
+ if (env.RLM_SYMBOLIC_COLLAB !== undefined) {
119
+ return { enabled: envFlagEnabled(env.RLM_SYMBOLIC_COLLAB), source: 'legacy' };
120
+ }
121
+ return { enabled: false, source: null };
122
+ }
105
123
  function shouldForceNonInteractive(env) {
106
124
  const stdinIsTTY = process.stdin?.isTTY === true;
107
125
  return (!stdinIsTTY ||
@@ -196,9 +214,9 @@ function resolveRlmMode(rawMode, options) {
196
214
  if (normalized !== 'auto') {
197
215
  return null;
198
216
  }
199
- if (options.delegated ||
200
- options.hasContextPath ||
201
- options.contextBytes >= options.symbolicMinBytes) {
217
+ const largeContext = options.contextBytes >= options.symbolicMinBytes;
218
+ const explicitContextSignal = options.hasContextPath || options.delegated;
219
+ if (largeContext && explicitContextSignal) {
202
220
  return 'symbolic';
203
221
  }
204
222
  return 'iterative';
@@ -323,9 +341,19 @@ async function runCodexCompletion(prompt, env, repoRoot, nonInteractive, subagen
323
341
  async function runCodexJsonlCompletion(prompt, env, repoRoot, nonInteractive, mirrorOutput, extraArgs = [], options = {}) {
324
342
  const { stdout, stderr } = await runCodexExec(['exec', '--json', ...extraArgs, prompt], env, repoRoot, nonInteractive, false, mirrorOutput);
325
343
  if (options.validateCollabLifecycle) {
326
- const validation = validateCollabLifecycle(stdout);
344
+ const rolePolicy = options.collabRolePolicy ?? DEFAULT_COLLAB_ROLE_POLICY;
345
+ const validation = validateCollabLifecycle(stdout, {
346
+ requireSpawnRole: rolePolicy !== 'off',
347
+ allowDefaultRole: options.collabAllowDefaultRole ?? false
348
+ });
327
349
  if (!validation.ok) {
328
- throw new Error(`Collab lifecycle validation failed: ${validation.reason}`);
350
+ const rolePolicyFailure = isRolePolicyValidationReason(validation.reasonCode);
351
+ if (rolePolicy === 'warn' && rolePolicyFailure) {
352
+ logger.warn(`Collab lifecycle validation warning: ${validation.reason}`);
353
+ }
354
+ else {
355
+ throw new Error(`Collab lifecycle validation failed: ${validation.reason}`);
356
+ }
329
357
  }
330
358
  }
331
359
  const message = extractAgentMessageFromJsonl(stdout);
@@ -334,6 +362,50 @@ async function runCodexJsonlCompletion(prompt, env, repoRoot, nonInteractive, mi
334
362
  }
335
363
  return [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
336
364
  }
365
+ function parseFeatureFlagsFromText(raw) {
366
+ const flags = {};
367
+ for (const line of raw.split(/\r?\n/u)) {
368
+ const trimmed = line.trim();
369
+ if (!trimmed) {
370
+ continue;
371
+ }
372
+ const tokens = trimmed.split(/\s+/u);
373
+ if (tokens.length < 2) {
374
+ continue;
375
+ }
376
+ const name = tokens[0] ?? '';
377
+ const enabledToken = tokens[tokens.length - 1] ?? '';
378
+ if (!name) {
379
+ continue;
380
+ }
381
+ if (enabledToken === 'true') {
382
+ flags[name] = true;
383
+ }
384
+ else if (enabledToken === 'false') {
385
+ flags[name] = false;
386
+ }
387
+ }
388
+ return flags;
389
+ }
390
+ function resolveCollabFeatureKeyFromFlags(flags) {
391
+ if (Object.prototype.hasOwnProperty.call(flags, COLLAB_FEATURE_CANONICAL)) {
392
+ return COLLAB_FEATURE_CANONICAL;
393
+ }
394
+ if (Object.prototype.hasOwnProperty.call(flags, COLLAB_FEATURE_LEGACY)) {
395
+ return COLLAB_FEATURE_LEGACY;
396
+ }
397
+ return COLLAB_FEATURE_LEGACY;
398
+ }
399
+ async function resolveCollabFeatureKey(env, repoRoot, nonInteractive) {
400
+ try {
401
+ const { stdout } = await runCodexExec(['features', 'list'], env, repoRoot, nonInteractive, false, false);
402
+ return resolveCollabFeatureKeyFromFlags(parseFeatureFlagsFromText(stdout));
403
+ }
404
+ catch (error) {
405
+ logger.debug(`Unable to resolve Codex collab feature key via \`codex features list\`: ${error instanceof Error ? error.message : String(error)}`);
406
+ return COLLAB_FEATURE_LEGACY;
407
+ }
408
+ }
337
409
  function extractAgentMessageFromJsonl(raw) {
338
410
  let lastMessage = null;
339
411
  const lines = raw.split(/\r?\n/);
@@ -380,12 +452,16 @@ function parseCollabToolCallsFromJsonl(raw) {
380
452
  const receiverThreadIds = Array.isArray(parsed.item.receiver_thread_ids)
381
453
  ? parsed.item.receiver_thread_ids.filter((entry) => typeof entry === 'string')
382
454
  : [];
455
+ const prompt = typeof parsed.item.prompt === 'string' ? parsed.item.prompt : null;
383
456
  calls.push({
384
457
  sequence: index,
385
458
  eventType: parsed.type,
386
459
  tool: parsed.item.tool,
387
460
  status: normalizeCollabStatus(parsed.item.status),
388
- receiverThreadIds
461
+ receiverThreadIds,
462
+ prompt,
463
+ agentType: normalizeCollabRoleToken(parsed.item.agent_type),
464
+ promptRole: resolveCollabRoleFromPrompt(prompt)
389
465
  });
390
466
  }
391
467
  catch {
@@ -397,6 +473,82 @@ function parseCollabToolCallsFromJsonl(raw) {
397
473
  function formatLifecycleIds(ids) {
398
474
  return ids.slice(0, 3).join(', ');
399
475
  }
476
+ function normalizeCollabRoleToken(value) {
477
+ if (typeof value !== 'string') {
478
+ return null;
479
+ }
480
+ const normalized = value.trim().toLowerCase();
481
+ if (!normalized || !COLLAB_ROLE_TOKEN_PATTERN.test(normalized)) {
482
+ return null;
483
+ }
484
+ return normalized;
485
+ }
486
+ function resolveCollabRoleFromPrompt(value) {
487
+ if (typeof value !== 'string') {
488
+ return null;
489
+ }
490
+ const match = value.match(COLLAB_ROLE_TAG_PATTERN);
491
+ if (!match || typeof match[1] !== 'string') {
492
+ return null;
493
+ }
494
+ return normalizeCollabRoleToken(match[1]);
495
+ }
496
+ function resolveCollabRolePolicy(value) {
497
+ const normalized = (value ?? '').trim().toLowerCase();
498
+ if (!normalized) {
499
+ return DEFAULT_COLLAB_ROLE_POLICY;
500
+ }
501
+ if (normalized === 'off' ||
502
+ normalized === 'disabled' ||
503
+ normalized === 'none' ||
504
+ normalized === '0' ||
505
+ normalized === 'false') {
506
+ return 'off';
507
+ }
508
+ if (normalized === 'warn' || normalized === 'warning' || normalized === 'soft') {
509
+ return 'warn';
510
+ }
511
+ if (normalized === 'enforce' || normalized === 'strict' || normalized === 'on' || normalized === 'true' || normalized === '1') {
512
+ return 'enforce';
513
+ }
514
+ logger.warn(`Invalid multi-agent role policy value "${value}". Using "${DEFAULT_COLLAB_ROLE_POLICY}" ` +
515
+ `(expected: enforce|warn|off; canonical env ${COLLAB_ROLE_POLICY_ENV_CANONICAL}, ` +
516
+ `legacy alias ${COLLAB_ROLE_POLICY_ENV_LEGACY}).`);
517
+ return DEFAULT_COLLAB_ROLE_POLICY;
518
+ }
519
+ function resolveSymbolicMultiAgentRolePolicyConfig(env) {
520
+ const canonical = env[COLLAB_ROLE_POLICY_ENV_CANONICAL];
521
+ const legacy = env[COLLAB_ROLE_POLICY_ENV_LEGACY];
522
+ if (canonical !== undefined) {
523
+ if (legacy !== undefined && legacy.trim().toLowerCase() !== canonical.trim().toLowerCase()) {
524
+ logger.warn(`${COLLAB_ROLE_POLICY_ENV_LEGACY} is ignored because ${COLLAB_ROLE_POLICY_ENV_CANONICAL} is set.`);
525
+ }
526
+ return { value: resolveCollabRolePolicy(canonical), source: 'canonical' };
527
+ }
528
+ if (legacy !== undefined) {
529
+ return { value: resolveCollabRolePolicy(legacy), source: 'legacy' };
530
+ }
531
+ return { value: resolveCollabRolePolicy(undefined), source: null };
532
+ }
533
+ function resolveSymbolicMultiAgentAllowDefaultRoleConfig(env) {
534
+ const canonical = env[COLLAB_ALLOW_DEFAULT_ROLE_ENV_CANONICAL];
535
+ const legacy = env[COLLAB_ALLOW_DEFAULT_ROLE_ENV_LEGACY];
536
+ if (canonical !== undefined) {
537
+ if (legacy !== undefined && envFlagEnabled(legacy) !== envFlagEnabled(canonical)) {
538
+ logger.warn(`${COLLAB_ALLOW_DEFAULT_ROLE_ENV_LEGACY} is ignored because ${COLLAB_ALLOW_DEFAULT_ROLE_ENV_CANONICAL} is set.`);
539
+ }
540
+ return { value: envFlagEnabled(canonical), source: 'canonical' };
541
+ }
542
+ if (legacy !== undefined) {
543
+ return { value: envFlagEnabled(legacy), source: 'legacy' };
544
+ }
545
+ return { value: false, source: null };
546
+ }
547
+ function isRolePolicyValidationReason(reasonCode) {
548
+ return (reasonCode === 'missing_role' ||
549
+ reasonCode === 'default_role_disallowed' ||
550
+ reasonCode === 'role_mismatch');
551
+ }
400
552
  function includesThreadLimit(text) {
401
553
  return text.toLowerCase().includes('agent thread limit reached');
402
554
  }
@@ -438,9 +590,11 @@ function hasCollabSpawnThreadLimitError(raw) {
438
590
  }
439
591
  return false;
440
592
  }
441
- function validateCollabLifecycle(raw) {
593
+ function validateCollabLifecycle(raw, options = {}) {
594
+ const requireSpawnRole = options.requireSpawnRole !== false;
595
+ const allowDefaultRole = options.allowDefaultRole === true;
442
596
  if (hasCollabSpawnThreadLimitError(raw)) {
443
- return { ok: false, reason: 'collab spawn hit thread limit' };
597
+ return { ok: false, reason: 'collab spawn hit thread limit', reasonCode: 'thread_limit' };
444
598
  }
445
599
  const calls = parseCollabToolCallsFromJsonl(raw);
446
600
  if (calls.length === 0) {
@@ -449,16 +603,53 @@ function validateCollabLifecycle(raw) {
449
603
  const spawnedAt = new Map();
450
604
  const waitedAt = new Map();
451
605
  const closedAt = new Map();
606
+ const missingRoleIds = new Set();
607
+ const disallowedDefaultRoleIds = new Set();
608
+ const mismatchedRoleIds = new Set();
609
+ const roleByThread = new Map();
452
610
  for (const call of calls) {
453
611
  const isCompleted = call.status === 'completed' || (call.status === 'unknown' && call.eventType === 'item.completed');
454
612
  if (!isCompleted) {
455
613
  continue;
456
614
  }
457
- for (const threadId of call.receiverThreadIds) {
458
- if (call.tool === 'spawn_agent' && !spawnedAt.has(threadId)) {
459
- spawnedAt.set(threadId, call.sequence);
615
+ if (call.tool === 'spawn_agent') {
616
+ const explicitRole = call.agentType;
617
+ const promptRole = call.promptRole;
618
+ const effectiveRole = explicitRole ?? promptRole;
619
+ const roleTargets = call.receiverThreadIds.length > 0 ? call.receiverThreadIds : [`spawn@${call.sequence}`];
620
+ if (requireSpawnRole && !effectiveRole) {
621
+ for (const target of roleTargets) {
622
+ missingRoleIds.add(target);
623
+ }
624
+ }
625
+ else if (effectiveRole === 'default' && !allowDefaultRole) {
626
+ for (const target of roleTargets) {
627
+ disallowedDefaultRoleIds.add(target);
628
+ }
629
+ }
630
+ if (explicitRole && promptRole && explicitRole !== promptRole) {
631
+ for (const target of roleTargets) {
632
+ mismatchedRoleIds.add(target);
633
+ }
460
634
  }
461
- else if (call.tool === 'wait') {
635
+ for (const threadId of call.receiverThreadIds) {
636
+ if (!spawnedAt.has(threadId)) {
637
+ spawnedAt.set(threadId, call.sequence);
638
+ }
639
+ if (!effectiveRole) {
640
+ continue;
641
+ }
642
+ const previous = roleByThread.get(threadId);
643
+ if (previous && previous !== effectiveRole) {
644
+ mismatchedRoleIds.add(threadId);
645
+ continue;
646
+ }
647
+ roleByThread.set(threadId, effectiveRole);
648
+ }
649
+ continue;
650
+ }
651
+ for (const threadId of call.receiverThreadIds) {
652
+ if (call.tool === 'wait') {
462
653
  waitedAt.set(threadId, call.sequence);
463
654
  }
464
655
  else if (call.tool === 'close_agent') {
@@ -467,35 +658,63 @@ function validateCollabLifecycle(raw) {
467
658
  }
468
659
  }
469
660
  const spawnedIds = Array.from(spawnedAt.keys());
470
- if (spawnedIds.length === 0) {
661
+ if (spawnedIds.length > 0) {
662
+ const missingWait = spawnedIds.filter((threadId) => !waitedAt.has(threadId));
663
+ if (missingWait.length > 0) {
664
+ return {
665
+ ok: false,
666
+ reason: `missing wait for spawned agent(s): ${formatLifecycleIds(missingWait)}`,
667
+ reasonCode: 'missing_wait'
668
+ };
669
+ }
670
+ const missingClose = spawnedIds.filter((threadId) => !closedAt.has(threadId));
671
+ if (missingClose.length > 0) {
672
+ return {
673
+ ok: false,
674
+ reason: `missing close_agent for spawned agent(s): ${formatLifecycleIds(missingClose)}`,
675
+ reasonCode: 'missing_close'
676
+ };
677
+ }
678
+ const invalidOrder = spawnedIds.filter((threadId) => {
679
+ const waitSequence = waitedAt.get(threadId);
680
+ const closeSequence = closedAt.get(threadId);
681
+ if (waitSequence === undefined || closeSequence === undefined) {
682
+ return false;
683
+ }
684
+ return closeSequence < waitSequence;
685
+ });
686
+ if (invalidOrder.length > 0) {
687
+ return {
688
+ ok: false,
689
+ reason: `close_agent before wait for agent(s): ${formatLifecycleIds(invalidOrder)}`,
690
+ reasonCode: 'close_before_wait'
691
+ };
692
+ }
693
+ }
694
+ if (!requireSpawnRole) {
471
695
  return { ok: true };
472
696
  }
473
- const missingWait = spawnedIds.filter((threadId) => !waitedAt.has(threadId));
474
- if (missingWait.length > 0) {
697
+ if (missingRoleIds.size > 0) {
475
698
  return {
476
699
  ok: false,
477
- reason: `missing wait for spawned agent(s): ${formatLifecycleIds(missingWait)}`
700
+ reason: `missing explicit role for spawn_agent call(s): ${formatLifecycleIds(Array.from(missingRoleIds))}. ` +
701
+ 'Prefix prompts with [agent_type:<role>] and set spawn_agent.agent_type when supported.',
702
+ reasonCode: 'missing_role'
478
703
  };
479
704
  }
480
- const missingClose = spawnedIds.filter((threadId) => !closedAt.has(threadId));
481
- if (missingClose.length > 0) {
705
+ if (disallowedDefaultRoleIds.size > 0) {
482
706
  return {
483
707
  ok: false,
484
- reason: `missing close_agent for spawned agent(s): ${formatLifecycleIds(missingClose)}`
708
+ reason: `spawn_agent used disallowed default role for: ${formatLifecycleIds(Array.from(disallowedDefaultRoleIds))}. ` +
709
+ 'Set a non-default agent_type explicitly.',
710
+ reasonCode: 'default_role_disallowed'
485
711
  };
486
712
  }
487
- const invalidOrder = spawnedIds.filter((threadId) => {
488
- const waitSequence = waitedAt.get(threadId);
489
- const closeSequence = closedAt.get(threadId);
490
- if (waitSequence === undefined || closeSequence === undefined) {
491
- return false;
492
- }
493
- return closeSequence < waitSequence;
494
- });
495
- if (invalidOrder.length > 0) {
713
+ if (mismatchedRoleIds.size > 0) {
496
714
  return {
497
715
  ok: false,
498
- reason: `close_agent before wait for agent(s): ${formatLifecycleIds(invalidOrder)}`
716
+ reason: `spawn_agent role mismatch for agent(s): ${formatLifecycleIds(Array.from(mismatchedRoleIds))}`,
717
+ reasonCode: 'role_mismatch'
499
718
  };
500
719
  }
501
720
  return { ok: true };
@@ -504,7 +723,8 @@ function buildCollabSubcallPrompt(prompt) {
504
723
  return [
505
724
  'Use collab tools to run the sub-agent prompt below.',
506
725
  'For every spawned agent id, execute this lifecycle in order:',
507
- '1) spawn_agent',
726
+ '1) spawn_agent with explicit agent_type (never omit it; omission defaults to `default`),',
727
+ ' and prefix the spawned prompt with [agent_type:<same-role>] on the first line',
508
728
  '2) wait (for that same id)',
509
729
  '3) close_agent (for that same id)',
510
730
  'Never leave spawned agents unclosed, including timeout or error paths.',
@@ -772,7 +992,12 @@ async function main() {
772
992
  logger.info(`Validator: ${validatorCommand}`);
773
993
  }
774
994
  const subagentsEnabled = envFlagEnabled(env.CODEX_SUBAGENTS) || envFlagEnabled(env.RLM_SUBAGENTS);
775
- const symbolicCollabEnabled = envFlagEnabled(env.RLM_SYMBOLIC_COLLAB);
995
+ const symbolicMultiAgent = resolveSymbolicMultiAgentConfig(env);
996
+ const symbolicCollabEnabled = symbolicMultiAgent.enabled;
997
+ const collabRolePolicyConfig = resolveSymbolicMultiAgentRolePolicyConfig(env);
998
+ const collabRolePolicy = collabRolePolicyConfig.value;
999
+ const collabAllowDefaultRoleConfig = resolveSymbolicMultiAgentAllowDefaultRoleConfig(env);
1000
+ const collabAllowDefaultRole = collabAllowDefaultRoleConfig.value;
776
1001
  const symbolicDeliberationEnabled = env.RLM_SYMBOLIC_DELIBERATION === undefined
777
1002
  ? true
778
1003
  : envFlagEnabled(env.RLM_SYMBOLIC_DELIBERATION);
@@ -782,6 +1007,22 @@ async function main() {
782
1007
  const symbolicDeliberationLogArtifacts = envFlagEnabled(env.RLM_SYMBOLIC_DELIBERATION_LOG);
783
1008
  const nonInteractive = shouldForceNonInteractive(env);
784
1009
  if (mode === 'symbolic') {
1010
+ if (symbolicMultiAgent.source === 'legacy') {
1011
+ logger.warn('RLM_SYMBOLIC_COLLAB is a legacy alias; prefer RLM_SYMBOLIC_MULTI_AGENT.');
1012
+ }
1013
+ if (collabRolePolicyConfig.source === 'legacy') {
1014
+ logger.warn(`${COLLAB_ROLE_POLICY_ENV_LEGACY} is a legacy alias; prefer ${COLLAB_ROLE_POLICY_ENV_CANONICAL}.`);
1015
+ }
1016
+ if (collabAllowDefaultRoleConfig.source === 'legacy') {
1017
+ logger.warn(`${COLLAB_ALLOW_DEFAULT_ROLE_ENV_LEGACY} is a legacy alias; prefer ${COLLAB_ALLOW_DEFAULT_ROLE_ENV_CANONICAL}.`);
1018
+ }
1019
+ const collabFeatureKey = symbolicCollabEnabled
1020
+ ? await resolveCollabFeatureKey(env, repoRoot, nonInteractive)
1021
+ : COLLAB_FEATURE_LEGACY;
1022
+ if (symbolicCollabEnabled) {
1023
+ logger.info(`Symbolic collab feature key: ${collabFeatureKey}`);
1024
+ logger.info(`Symbolic collab role policy: ${collabRolePolicy} (allow_default_role=${collabAllowDefaultRole ? '1' : '0'})`);
1025
+ }
785
1026
  const budgets = {
786
1027
  maxSubcallsPerIteration: parsePositiveInt(env.RLM_MAX_SUBCALLS_PER_ITERATION, DEFAULT_MAX_SUBCALLS_PER_ITERATION) ??
787
1028
  0,
@@ -881,11 +1122,13 @@ async function main() {
881
1122
  const collabPrompt = buildCollabSubcallPrompt(prompt);
882
1123
  return runCodexJsonlCompletion(collabPrompt, env, repoRoot, nonInteractive, true, [
883
1124
  '--enable',
884
- 'collab',
1125
+ collabFeatureKey,
885
1126
  '--sandbox',
886
1127
  'read-only'
887
1128
  ], {
888
- validateCollabLifecycle: true
1129
+ validateCollabLifecycle: true,
1130
+ collabRolePolicy,
1131
+ collabAllowDefaultRole
889
1132
  });
890
1133
  },
891
1134
  deliberation: {
@@ -904,11 +1147,13 @@ async function main() {
904
1147
  const collabPrompt = buildCollabSubcallPrompt(prompt);
905
1148
  return runCodexJsonlCompletion(collabPrompt, env, repoRoot, nonInteractive, true, [
906
1149
  '--enable',
907
- 'collab',
1150
+ collabFeatureKey,
908
1151
  '--sandbox',
909
1152
  'read-only'
910
1153
  ], {
911
- validateCollabLifecycle: true
1154
+ validateCollabLifecycle: true,
1155
+ collabRolePolicy,
1156
+ collabAllowDefaultRole
912
1157
  });
913
1158
  }
914
1159
  },
@@ -956,10 +1201,19 @@ if (entry && entry === self) {
956
1201
  export const __test__ = {
957
1202
  parseMaxIterations,
958
1203
  parsePositiveInt,
1204
+ resolveSymbolicMultiAgentConfig,
1205
+ resolveSymbolicMultiAgentRolePolicyConfig,
1206
+ resolveSymbolicMultiAgentAllowDefaultRoleConfig,
1207
+ parseFeatureFlagsFromText,
1208
+ resolveCollabFeatureKeyFromFlags,
1209
+ resolveCollabRolePolicy,
1210
+ isRolePolicyValidationReason,
959
1211
  resolveRlmMode,
960
1212
  parseCollabToolCallsFromJsonl,
961
1213
  validateCollabLifecycle,
962
1214
  buildCollabSubcallPrompt,
1215
+ COLLAB_FEATURE_CANONICAL,
1216
+ COLLAB_FEATURE_LEGACY,
963
1217
  DEFAULT_MAX_ITERATIONS,
964
1218
  DEFAULT_MAX_MINUTES,
965
1219
  DEFAULT_SYMBOLIC_MIN_BYTES
@@ -58,6 +58,7 @@ export async function bootstrapManifest(runId, options) {
58
58
  prompt_packs: [],
59
59
  guardrails_required: pipeline.guardrailsRequired !== false,
60
60
  cloud_execution: null,
61
+ cloud_fallback: null,
61
62
  learning: {
62
63
  validation: {
63
64
  mode: 'per-task',
@@ -192,6 +193,7 @@ export function resetForResume(manifest) {
192
193
  manifest.status_detail = 'resuming';
193
194
  manifest.guardrail_status = undefined;
194
195
  manifest.cloud_execution = null;
196
+ manifest.cloud_fallback = null;
195
197
  }
196
198
  export function recordResumeEvent(manifest, event) {
197
199
  manifest.resume_events.push({ ...event, timestamp: isoTimestamp() });
@@ -228,18 +230,20 @@ function computeGuardrailStatus(manifest) {
228
230
  other: 0
229
231
  };
230
232
  for (const entry of guardrailCommands) {
231
- if (entry.status === 'succeeded') {
233
+ const status = classifyGuardrailCommand(entry);
234
+ if (status === 'succeeded') {
232
235
  counts.succeeded += 1;
236
+ continue;
233
237
  }
234
- else if (entry.status === 'failed') {
238
+ if (status === 'failed') {
235
239
  counts.failed += 1;
240
+ continue;
236
241
  }
237
- else if (entry.status === 'skipped') {
242
+ if (status === 'skipped') {
238
243
  counts.skipped += 1;
244
+ continue;
239
245
  }
240
- else {
241
- counts.other += 1;
242
- }
246
+ counts.other += 1;
243
247
  }
244
248
  const present = counts.succeeded > 0;
245
249
  let recommendation = null;
@@ -270,6 +274,27 @@ function selectGuardrailCommands(manifest) {
270
274
  return haystack.includes('spec-guard') || haystack.includes('specguardrunner');
271
275
  });
272
276
  }
277
+ function classifyGuardrailCommand(entry) {
278
+ if (entry.status === 'failed') {
279
+ return 'failed';
280
+ }
281
+ if (entry.status === 'skipped') {
282
+ return 'skipped';
283
+ }
284
+ if (entry.status === 'succeeded') {
285
+ return isExplicitGuardrailSkip(entry.summary) ? 'skipped' : 'succeeded';
286
+ }
287
+ return 'other';
288
+ }
289
+ function isExplicitGuardrailSkip(summary) {
290
+ const normalized = summary?.toLowerCase() ?? '';
291
+ if (!normalized) {
292
+ return false;
293
+ }
294
+ return (normalized.includes('[spec-guard] skipped') ||
295
+ normalized.includes('spec-guard skipped') ||
296
+ normalized.includes('spec guard skipped'));
297
+ }
273
298
  function formatGuardrailSummary(counts) {
274
299
  if (counts.total === 0) {
275
300
  return 'Guardrails: spec-guard command not found.';
@@ -53,11 +53,19 @@ export async function runCommandStage(context, hooks = {}) {
53
53
  let stderrTruncated = false;
54
54
  let collabBuffer = '';
55
55
  let collabCount = manifest.collab_tool_calls?.length ?? 0;
56
+ const manifestCaptureLimit = typeof manifest.collab_tool_calls_max_events === 'number'
57
+ ? Math.max(0, Math.trunc(manifest.collab_tool_calls_max_events))
58
+ : null;
59
+ const hasLegacyUnknownCaptureHistory = manifestCaptureLimit === null && collabCount > 0;
60
+ const runCollabCaptureLimit = manifestCaptureLimit ?? Math.max(0, MAX_COLLAB_TOOL_CALLS);
61
+ if (!hasLegacyUnknownCaptureHistory) {
62
+ manifest.collab_tool_calls_max_events = runCollabCaptureLimit;
63
+ }
56
64
  const recordCollabToolCall = (record) => {
57
- if (MAX_COLLAB_TOOL_CALLS <= 0) {
65
+ if (runCollabCaptureLimit <= 0) {
58
66
  return;
59
67
  }
60
- if (collabCount >= MAX_COLLAB_TOOL_CALLS) {
68
+ if (collabCount >= runCollabCaptureLimit) {
61
69
  return;
62
70
  }
63
71
  if (!manifest.collab_tool_calls) {