oh-my-codex 0.15.2 → 0.15.3

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 (166) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/dist/agents/__tests__/native-config.test.js +33 -0
  4. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  5. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +9 -1
  6. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +2 -0
  8. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +1 -0
  9. package/dist/cli/__tests__/doctor-context-window-warning.test.js +122 -0
  10. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -0
  11. package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -2
  12. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  13. package/dist/cli/__tests__/exec.test.js +1 -0
  14. package/dist/cli/__tests__/exec.test.js.map +1 -1
  15. package/dist/cli/__tests__/explore.test.js +40 -17
  16. package/dist/cli/__tests__/explore.test.js.map +1 -1
  17. package/dist/cli/__tests__/index.test.js +141 -8
  18. package/dist/cli/__tests__/index.test.js.map +1 -1
  19. package/dist/cli/__tests__/mcp-serve.test.js +27 -1
  20. package/dist/cli/__tests__/mcp-serve.test.js.map +1 -1
  21. package/dist/cli/__tests__/ralph.test.js +59 -1
  22. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  23. package/dist/cli/__tests__/setup-scope.test.js +2 -1
  24. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  25. package/dist/cli/__tests__/team.test.js +55 -10
  26. package/dist/cli/__tests__/team.test.js.map +1 -1
  27. package/dist/cli/doctor.d.ts.map +1 -1
  28. package/dist/cli/doctor.js +46 -3
  29. package/dist/cli/doctor.js.map +1 -1
  30. package/dist/cli/index.d.ts +16 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +126 -15
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/mcp-serve.d.ts +1 -0
  35. package/dist/cli/mcp-serve.d.ts.map +1 -1
  36. package/dist/cli/mcp-serve.js +8 -0
  37. package/dist/cli/mcp-serve.js.map +1 -1
  38. package/dist/cli/ralph.d.ts +2 -0
  39. package/dist/cli/ralph.d.ts.map +1 -1
  40. package/dist/cli/ralph.js +17 -1
  41. package/dist/cli/ralph.js.map +1 -1
  42. package/dist/cli/team.d.ts +4 -0
  43. package/dist/cli/team.d.ts.map +1 -1
  44. package/dist/cli/team.js +47 -22
  45. package/dist/cli/team.js.map +1 -1
  46. package/dist/config/__tests__/generator-idempotent.test.js +27 -5
  47. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  48. package/dist/config/generator.d.ts +11 -2
  49. package/dist/config/generator.d.ts.map +1 -1
  50. package/dist/config/generator.js +114 -58
  51. package/dist/config/generator.js.map +1 -1
  52. package/dist/hooks/__tests__/agents-overlay.test.js +59 -0
  53. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  54. package/dist/hooks/__tests__/anti-slop-workflow.test.js +109 -18
  55. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  56. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  57. package/dist/hooks/agents-overlay.js +21 -0
  58. package/dist/hooks/agents-overlay.js.map +1 -1
  59. package/dist/hud/__tests__/index.test.js +30 -14
  60. package/dist/hud/__tests__/index.test.js.map +1 -1
  61. package/dist/openclaw/__tests__/dispatcher.test.js +1 -1
  62. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  63. package/dist/pipeline/__tests__/stages.test.js +398 -14
  64. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  65. package/dist/pipeline/stages/team-exec.d.ts +8 -4
  66. package/dist/pipeline/stages/team-exec.d.ts.map +1 -1
  67. package/dist/pipeline/stages/team-exec.js +198 -13
  68. package/dist/pipeline/stages/team-exec.js.map +1 -1
  69. package/dist/planning/__tests__/artifacts.test.js +246 -1
  70. package/dist/planning/__tests__/artifacts.test.js.map +1 -1
  71. package/dist/planning/artifact-names.d.ts +13 -0
  72. package/dist/planning/artifact-names.d.ts.map +1 -0
  73. package/dist/planning/artifact-names.js +108 -0
  74. package/dist/planning/artifact-names.js.map +1 -0
  75. package/dist/planning/artifacts.d.ts +22 -1
  76. package/dist/planning/artifacts.d.ts.map +1 -1
  77. package/dist/planning/artifacts.js +165 -50
  78. package/dist/planning/artifacts.js.map +1 -1
  79. package/dist/ralph/__tests__/persistence.test.js +21 -1
  80. package/dist/ralph/__tests__/persistence.test.js.map +1 -1
  81. package/dist/ralph/persistence.d.ts.map +1 -1
  82. package/dist/ralph/persistence.js +6 -4
  83. package/dist/ralph/persistence.js.map +1 -1
  84. package/dist/scripts/__tests__/codex-native-hook.test.js +352 -2
  85. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  86. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  87. package/dist/scripts/codex-native-hook.js +85 -6
  88. package/dist/scripts/codex-native-hook.js.map +1 -1
  89. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  90. package/dist/scripts/codex-native-pre-post.js +123 -0
  91. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  92. package/dist/scripts/notify-hook/team-worker-posttooluse.js +1 -1
  93. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  94. package/dist/scripts/notify-hook.js +1 -1
  95. package/dist/scripts/notify-hook.js.map +1 -1
  96. package/dist/scripts/sync-plugin-mirror.d.ts +1 -0
  97. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  98. package/dist/scripts/sync-plugin-mirror.js +8 -2
  99. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  100. package/dist/state/__tests__/skill-active.test.js +41 -0
  101. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  102. package/dist/team/__tests__/api-interop.test.js +220 -0
  103. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  104. package/dist/team/__tests__/model-contract.test.js +40 -9
  105. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  106. package/dist/team/__tests__/repo-aware-decomposition.test.js +41 -0
  107. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  108. package/dist/team/__tests__/runtime-cli.test.js +24 -0
  109. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  110. package/dist/team/__tests__/runtime.test.js +446 -67
  111. package/dist/team/__tests__/runtime.test.js.map +1 -1
  112. package/dist/team/__tests__/state.test.js +13 -0
  113. package/dist/team/__tests__/state.test.js.map +1 -1
  114. package/dist/team/__tests__/team-identity.test.d.ts +2 -0
  115. package/dist/team/__tests__/team-identity.test.d.ts.map +1 -0
  116. package/dist/team/__tests__/team-identity.test.js +166 -0
  117. package/dist/team/__tests__/team-identity.test.js.map +1 -0
  118. package/dist/team/__tests__/tmux-session.test.js +55 -1
  119. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  120. package/dist/team/__tests__/worker-bootstrap.test.js +12 -0
  121. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  122. package/dist/team/api-interop.d.ts +1 -0
  123. package/dist/team/api-interop.d.ts.map +1 -1
  124. package/dist/team/api-interop.js +159 -129
  125. package/dist/team/api-interop.js.map +1 -1
  126. package/dist/team/delivery-log.d.ts +1 -1
  127. package/dist/team/delivery-log.d.ts.map +1 -1
  128. package/dist/team/delivery-log.js.map +1 -1
  129. package/dist/team/repo-aware-decomposition.d.ts +3 -0
  130. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  131. package/dist/team/repo-aware-decomposition.js +2 -0
  132. package/dist/team/repo-aware-decomposition.js.map +1 -1
  133. package/dist/team/runtime-cli.d.ts +32 -2
  134. package/dist/team/runtime-cli.d.ts.map +1 -1
  135. package/dist/team/runtime-cli.js +78 -26
  136. package/dist/team/runtime-cli.js.map +1 -1
  137. package/dist/team/runtime.d.ts +1 -1
  138. package/dist/team/runtime.d.ts.map +1 -1
  139. package/dist/team/runtime.js +338 -35
  140. package/dist/team/runtime.js.map +1 -1
  141. package/dist/team/state.d.ts +9 -0
  142. package/dist/team/state.d.ts.map +1 -1
  143. package/dist/team/state.js +21 -0
  144. package/dist/team/state.js.map +1 -1
  145. package/dist/team/team-identity.d.ts +26 -0
  146. package/dist/team/team-identity.d.ts.map +1 -0
  147. package/dist/team/team-identity.js +169 -0
  148. package/dist/team/team-identity.js.map +1 -0
  149. package/dist/team/tmux-session.d.ts +18 -0
  150. package/dist/team/tmux-session.d.ts.map +1 -1
  151. package/dist/team/tmux-session.js +61 -1
  152. package/dist/team/tmux-session.js.map +1 -1
  153. package/dist/team/worker-bootstrap.d.ts +2 -0
  154. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  155. package/dist/team/worker-bootstrap.js +10 -1
  156. package/dist/team/worker-bootstrap.js.map +1 -1
  157. package/package.json +1 -1
  158. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  159. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +30 -5
  160. package/skills/ai-slop-cleaner/SKILL.md +30 -5
  161. package/src/scripts/__tests__/codex-native-hook.test.ts +398 -2
  162. package/src/scripts/codex-native-hook.ts +115 -5
  163. package/src/scripts/codex-native-pre-post.ts +121 -0
  164. package/src/scripts/notify-hook/team-worker-posttooluse.ts +1 -1
  165. package/src/scripts/notify-hook.ts +1 -1
  166. package/src/scripts/sync-plugin-mirror.ts +11 -2
@@ -3,8 +3,8 @@ import { existsSync, appendFileSync, mkdirSync } from 'fs';
3
3
  import { mkdir, readdir, readFile, writeFile } from 'fs/promises';
4
4
  import { performance } from 'perf_hooks';
5
5
  import { spawn, spawnSync } from 'child_process';
6
- import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, buildWorkerProcessLaunchSpec, resolveTeamWorkerCli, resolveTeamWorkerCliPlan, resolveTeamWorkerLaunchMode, waitForWorkerReady, waitForWorkerReadyAsync, dismissTrustPromptIfPresent, sendToWorker, sendToWorkerStdin, isWorkerAlive, isWorkerPaneOpen, getWorkerPanePid, killWorkerByPaneIdAsync, restoreStandaloneHudPane, teardownWorkerPanes, unregisterResizeHook, destroyTeamSession, listPaneIds, listTeamSessions, resolveSharedSessionShutdownTopology, } from './tmux-session.js';
7
- import { teamInit as initTeamState, DEFAULT_MAX_WORKERS, teamReadConfig as readTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadWorkerHeartbeat as readWorkerHeartbeat, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerInbox as writeWorkerInbox, teamCreateTask as createStateTask, teamReadTask as readTask, teamListTasks as listTasks, teamUpdateTask as updateTask, teamReadManifest as readTeamManifestV2, teamNormalizeGovernance as normalizeTeamGovernance, teamNormalizePolicy as normalizeTeamPolicy, teamClaimTask as claimTask, teamReleaseTaskClaim as releaseTaskClaim, teamReclaimExpiredTaskClaim as reclaimExpiredTaskClaim, teamAppendEvent as appendTeamEvent, teamReadTaskApproval as readTaskApproval, teamListMailbox as listMailboxMessages, teamMarkMessageDelivered as markMessageDelivered, teamMarkMessageNotified as markMessageNotified, teamEnqueueDispatchRequest as enqueueDispatchRequest, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamTransitionDispatchRequest as transitionDispatchRequest, teamReadDispatchRequest as readDispatchRequest, teamCleanup as cleanupTeamState, teamSaveConfig as saveTeamConfig, teamWriteShutdownRequest as writeShutdownRequest, teamReadShutdownAck as readShutdownAck, teamReadMonitorSnapshot as readMonitorSnapshot, teamWriteMonitorSnapshot as writeMonitorSnapshot, teamReadPhase as readTeamPhaseState, teamWritePhase as writeTeamPhaseState, teamWriteWorkerStatus as writeWorkerStatus, } from './team-ops.js';
6
+ import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, buildWorkerProcessLaunchSpec, resolveTeamWorkerCli, resolveTeamWorkerCliPlan, resolveTeamWorkerLaunchMode, waitForWorkerReady, waitForWorkerReadyAsync, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafety, sendToWorker, sendToWorkerStdin, isWorkerAlive, isWorkerPaneOpen, getWorkerPanePid, killWorkerByPaneIdAsync, restoreStandaloneHudPane, teardownWorkerPanes, unregisterResizeHook, destroyTeamSession, listPaneIds, listTeamSessions, resolveSharedSessionShutdownTopology, } from './tmux-session.js';
7
+ import { teamInit as initTeamState, DEFAULT_MAX_WORKERS, teamReadConfig as readTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadWorkerHeartbeat as readWorkerHeartbeat, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerInbox as writeWorkerInbox, teamCreateTask as createStateTask, teamReadTask as readTask, teamListTasks as listTasks, teamUpdateTask as updateTask, teamReadManifest as readTeamManifestV2, teamNormalizeGovernance as normalizeTeamGovernance, teamNormalizePolicy as normalizeTeamPolicy, teamClaimTask as claimTask, teamReleaseTaskClaim as releaseTaskClaim, teamReclaimExpiredTaskClaim as reclaimExpiredTaskClaim, teamAppendEvent as appendTeamEvent, teamReadTaskApproval as readTaskApproval, teamListMailbox as listMailboxMessages, teamMarkMessageDelivered as markMessageDelivered, teamMarkMessageNotified as markMessageNotified, teamEnqueueDispatchRequest as enqueueDispatchRequest, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamTransitionDispatchRequest as transitionDispatchRequest, teamReadDispatchRequest as readDispatchRequest, teamCleanup as cleanupTeamState, teamSaveConfig as saveTeamConfig, teamWriteShutdownRequest as writeShutdownRequest, teamReadShutdownAck as readShutdownAck, teamReadMonitorSnapshot as readMonitorSnapshot, teamWriteMonitorSnapshot as writeMonitorSnapshot, teamReadPhase as readTeamPhaseState, teamWritePhase as writeTeamPhaseState, teamWriteWorkerStatus as writeWorkerStatus, writeAtomic, } from './team-ops.js';
8
8
  import { queueInboxInstruction, queueDirectMailboxMessage, queueBroadcastMailboxMessage, waitForDispatchReceipt, } from './mcp-comm.js';
9
9
  import { appendTeamDeliveryLogForCwd } from './delivery-log.js';
10
10
  import { remapRepoAwareDecompositionMetadataToCreatedTasks, } from './repo-aware-decomposition.js';
@@ -16,6 +16,7 @@ import { codexPromptsDir } from '../utils/paths.js';
16
16
  import { isTerminalPhase } from './orchestrator.js';
17
17
  import { resolveTeamWorkerLaunchArgs, TEAM_LOW_COMPLEXITY_DEFAULT_MODEL, parseTeamWorkerLaunchArgs, splitWorkerLaunchArgs, resolveAgentDefaultModel, resolveAgentReasoningEffort, } from './model-contract.js';
18
18
  import { resolveCanonicalTeamStateRoot } from './state-root.js';
19
+ import { buildInternalTeamName, resolveTeamIdentityScope, resolveTeamNameForCurrentContext } from './team-identity.js';
19
20
  import { inferPhaseTargetFromTaskCounts, reconcilePhaseStateForMonitor } from './phase-controller.js';
20
21
  import { getTeamTmuxSessions } from '../notifications/tmux.js';
21
22
  import { hasStructuredVerificationEvidence } from '../verification/verifier.js';
@@ -188,6 +189,21 @@ async function logRuntimeDispatchOutcome(params) {
188
189
  reason: outcome.reason,
189
190
  });
190
191
  }
192
+ async function logStartupTiming(params) {
193
+ const { cwd, teamName, workerName, event, paneId, elapsedMs, reason, requestId, transport } = params;
194
+ await appendTeamDeliveryLogForCwd(cwd, {
195
+ event: 'startup_timing',
196
+ source: 'team.runtime',
197
+ team: teamName,
198
+ to_worker: workerName,
199
+ startup_event: event,
200
+ pane_id: paneId,
201
+ elapsed_ms: typeof elapsedMs === 'number' ? Math.round(elapsedMs) : undefined,
202
+ reason,
203
+ request_id: requestId,
204
+ transport,
205
+ });
206
+ }
191
207
  function collectProvisionedShutdownWorktrees(config) {
192
208
  const seenWorktreePaths = new Set();
193
209
  const worktrees = [];
@@ -932,6 +948,40 @@ const WORKTREE_TRIGGER_STATE_ROOT = '$OMX_TEAM_STATE_ROOT';
932
948
  const STARTUP_EVIDENCE_TIMEOUT_MS = 15_000;
933
949
  const STARTUP_EVIDENCE_POLL_MS = 100;
934
950
  const STARTUP_EVIDENCE_LAUNCH_TIMEOUT_MS = 45_000;
951
+ const STARTUP_TIMING_LOG_VERSION = 1;
952
+ function createStartupTimingRecorder(teamName, cwd) {
953
+ const startedAt = performance.now();
954
+ const events = [];
955
+ return {
956
+ mark: (phase, details = {}) => {
957
+ events.push({
958
+ phase,
959
+ at: new Date().toISOString(),
960
+ elapsed_ms: Math.round((performance.now() - startedAt) * 1000) / 1000,
961
+ ...details,
962
+ });
963
+ },
964
+ flush: async () => {
965
+ const timingPath = join(cwd, '.omx', 'state', 'team', teamName, 'startup-timing.json');
966
+ await writeAtomic(timingPath, JSON.stringify({ schema_version: STARTUP_TIMING_LOG_VERSION, team_name: teamName, events }, null, 2)).catch(() => { });
967
+ for (const event of events) {
968
+ await appendTeamDeliveryLogForCwd(cwd, {
969
+ event: 'dispatch_result',
970
+ source: 'team.runtime.startup-timing',
971
+ team: teamName,
972
+ result: event.ok === false ? 'failed' : 'ok',
973
+ phase: event.phase,
974
+ elapsed_ms: event.elapsed_ms,
975
+ to_worker: event.worker,
976
+ pane_id: event.pane_id,
977
+ reason: event.reason,
978
+ transport: event.transport,
979
+ request_id: event.request_id,
980
+ });
981
+ }
982
+ },
983
+ };
984
+ }
935
985
  const promptWorkerRegistry = new Map();
936
986
  const previousModelInstructionsFileByTeam = new Map();
937
987
  const PROMPT_WORKER_SIGTERM_WAIT_MS = 3_000;
@@ -1000,7 +1050,7 @@ function resolveGovernancePolicy(governance, legacyPolicy) {
1000
1050
  return normalizeTeamGovernance(governance, legacyPolicy);
1001
1051
  }
1002
1052
  async function assertNestedTeamAllowed(cwd) {
1003
- const workerContext = parseTeamWorkerContext(process.env.OMX_TEAM_WORKER);
1053
+ const workerContext = parseTeamWorkerContext(process.env.OMX_TEAM_INTERNAL_WORKER || process.env.OMX_TEAM_WORKER);
1004
1054
  if (!workerContext)
1005
1055
  return;
1006
1056
  for (const candidateCwd of resolveManifestLookupCwds(cwd)) {
@@ -1077,6 +1127,22 @@ async function recordRecoverableStartupIssue(params) {
1077
1127
  reason,
1078
1128
  }, cwd).catch(() => { });
1079
1129
  }
1130
+ async function recordPromptStartupWorkerStopped(params) {
1131
+ const { teamName, workerName, taskIds, reason, cwd } = params;
1132
+ const updatedAt = new Date().toISOString();
1133
+ await writeWorkerStatus(teamName, workerName, {
1134
+ state: 'failed',
1135
+ current_task_id: taskIds[0],
1136
+ reason,
1137
+ updated_at: updatedAt,
1138
+ }, cwd).catch(() => { });
1139
+ await appendTeamEvent(teamName, {
1140
+ type: 'worker_stopped',
1141
+ worker: workerName,
1142
+ task_id: taskIds[0],
1143
+ reason,
1144
+ }, cwd).catch(() => { });
1145
+ }
1080
1146
  function setTeamModelInstructionsFile(teamName, filePath) {
1081
1147
  if (!previousModelInstructionsFileByTeam.has(teamName)) {
1082
1148
  previousModelInstructionsFileByTeam.set(teamName, process.env[MODEL_INSTRUCTIONS_FILE_ENV]);
@@ -1481,11 +1547,28 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1481
1547
  const leaderCwd = resolve(cwd);
1482
1548
  await assertNestedTeamAllowed(leaderCwd);
1483
1549
  const effectiveWorktreeMode = resolveEffectiveTeamWorktreeMode(leaderCwd, options.worktreeMode);
1484
- const sanitized = sanitizeTeamName(teamName);
1485
- const leaderSessionId = await resolveLeaderSessionId(leaderCwd);
1486
- await assertTeamStartupIsNonDestructive(sanitized, leaderCwd, leaderSessionId);
1550
+ const displayName = sanitizeTeamName(teamName);
1487
1551
  const workerLaunchMode = resolveTeamWorkerLaunchMode(process.env);
1488
1552
  const displayMode = workerLaunchMode === 'interactive' ? 'split_pane' : 'auto';
1553
+ const rawIdentityScope = resolveTeamIdentityScope(process.env);
1554
+ const resolvedLeaderSessionId = await resolveLeaderSessionId(leaderCwd);
1555
+ const identityScope = rawIdentityScope.source === 'run-id'
1556
+ ? (resolvedLeaderSessionId
1557
+ ? { ...rawIdentityScope, sessionId: resolvedLeaderSessionId, runId: '' }
1558
+ : {
1559
+ ...rawIdentityScope,
1560
+ // Prompt-mode starts can run outside tmux/native session metadata. A fresh
1561
+ // random run id would give every start a new leader identity and bypass
1562
+ // one-active-team protection, so scope that fallback to the leader cwd.
1563
+ runId: `cwd:${leaderCwd}`,
1564
+ })
1565
+ : rawIdentityScope;
1566
+ const sanitized = buildInternalTeamName(displayName, identityScope);
1567
+ const leaderSessionId = identityScope.sessionId || identityScope.paneId || identityScope.tmuxTarget || identityScope.runId;
1568
+ await assertTeamStartupIsNonDestructive(sanitized, leaderCwd, leaderSessionId);
1569
+ if (displayName !== sanitized) {
1570
+ await assertTeamStartupIsNonDestructive(displayName, leaderCwd, leaderSessionId);
1571
+ }
1489
1572
  if (workerLaunchMode === 'interactive') {
1490
1573
  if (!isTmuxAvailable()) {
1491
1574
  throw new Error('Team mode requires tmux. Install with: apt install tmux / brew install tmux');
@@ -1553,10 +1636,18 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1553
1636
  const skipWorkerReadyWait = shouldSkipWorkerReadyWait(process.env);
1554
1637
  try {
1555
1638
  // 3. Init state directory + config
1556
- config = await initTeamState(sanitized, task, agentType, workerCount, leaderCwd, DEFAULT_MAX_WORKERS, { ...process.env, OMX_TEAM_DISPLAY_MODE: displayMode, OMX_TEAM_WORKER_LAUNCH_MODE: workerLaunchMode }, {
1639
+ config = await initTeamState(sanitized, task, agentType, workerCount, leaderCwd, DEFAULT_MAX_WORKERS, {
1640
+ ...process.env,
1641
+ OMX_SESSION_ID: leaderSessionId,
1642
+ OMX_TEAM_DISPLAY_MODE: displayMode,
1643
+ OMX_TEAM_WORKER_LAUNCH_MODE: workerLaunchMode,
1644
+ }, {
1557
1645
  leader_cwd: leaderCwd,
1558
1646
  team_state_root: teamStateRoot,
1559
1647
  workspace_mode: workspaceMode,
1648
+ display_name: displayName,
1649
+ requested_name: displayName,
1650
+ identity_source: identityScope.source,
1560
1651
  worktree_mode: effectiveWorktreeMode,
1561
1652
  }, 'default');
1562
1653
  if (!config) {
@@ -1565,6 +1656,9 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1565
1656
  config.leader_cwd = leaderCwd;
1566
1657
  config.team_state_root = teamStateRoot;
1567
1658
  config.workspace_mode = workspaceMode;
1659
+ config.display_name = displayName;
1660
+ config.requested_name = displayName;
1661
+ config.identity_source = identityScope.source;
1568
1662
  config.worktree_mode = effectiveWorktreeMode;
1569
1663
  // 4. Create tasks. Repo-aware DAG dependencies are symbolic until the
1570
1664
  // state layer returns concrete task IDs, so create those tasks dependency
@@ -1661,6 +1755,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1661
1755
  rolePromptContent: rawRolePromptContent ?? undefined,
1662
1756
  worktreeRootAgentsCanonical: Boolean(workerWorkspace.worktreePath),
1663
1757
  taskHints: effectiveDecompositionMetadata?.task_hints,
1758
+ approvedContextSummary: effectiveDecompositionMetadata?.approved_context_summary,
1664
1759
  });
1665
1760
  const triggerDirective = buildTriggerDirective(workerName, sanitized, resolveInstructionStateRoot(workerWorkspace.worktreePath));
1666
1761
  const trigger = triggerDirective.text;
@@ -1688,6 +1783,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1688
1783
  [TEAM_STATE_ROOT_ENV]: teamStateRoot,
1689
1784
  [TEAM_LEADER_CWD_ENV]: leaderCwd,
1690
1785
  [MODEL_INSTRUCTIONS_FILE_ENV]: plan.instructionsFilePath,
1786
+ OMX_TEAM_DISPLAY_NAME: displayName,
1691
1787
  };
1692
1788
  if (plan.workerWorkspace.worktreePath) {
1693
1789
  env.OMX_TEAM_WORKTREE_PATH = plan.workerWorkspace.worktreePath;
@@ -1756,6 +1852,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1756
1852
  cleanup: options.cleanupLaunchOrphanedMcpProcesses,
1757
1853
  writeWarning: options.writeCleanupWarning,
1758
1854
  });
1855
+ const startupTiming = createStartupTimingRecorder(sanitized, leaderCwd);
1759
1856
  if (workerLaunchMode === 'interactive') {
1760
1857
  const createdSession = createTeamSession(sanitized, workerCount, leaderCwd, sharedWorkerLaunchArgs, workerStartups);
1761
1858
  sessionName = createdSession.name;
@@ -1763,6 +1860,9 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1763
1860
  createdWorkerPaneIds.push(...createdSession.workerPaneIds);
1764
1861
  createdLeaderPaneId = createdSession.leaderPaneId;
1765
1862
  applyCreatedInteractiveSessionToConfig(config, createdSession, workerPaneIds);
1863
+ for (const [index, paneId] of createdSession.workerPaneIds.entries()) {
1864
+ startupTiming.mark('split_returned', { worker: `worker-${index + 1}`, pane_id: paneId });
1865
+ }
1766
1866
  }
1767
1867
  else {
1768
1868
  config.tmux_session = `prompt-${sanitized}`;
@@ -1787,6 +1887,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1787
1887
  throw new Error(`missing bootstrap plan for worker-${i}`);
1788
1888
  }
1789
1889
  await materializeWorkerStartupState(bootstrapPlan, i, workerPaneIds[i - 1]);
1890
+ startupTiming.mark('identity_inbox_written', { worker: bootstrapPlan.workerName, pane_id: workerPaneIds[i - 1] });
1790
1891
  }
1791
1892
  await saveTeamConfig(config, leaderCwd);
1792
1893
  // 7. Start all safe per-worker readiness/dispatch attempts concurrently.
@@ -1812,6 +1913,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1812
1913
  const trigger = bootstrapPlan.trigger;
1813
1914
  const triggerIntent = bootstrapPlan.triggerIntent;
1814
1915
  const initialPrompt = bootstrapPlan.initialPrompt;
1916
+ const startupStartedAt = performance.now();
1815
1917
  const taskRoles = workerTasks
1816
1918
  .map((task) => task.role)
1817
1919
  .filter((role) => Boolean(role));
@@ -1819,8 +1921,26 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1819
1921
  if (uniqueTaskRoles.length > 1) {
1820
1922
  console.log(`[omx:team] ${workerName}: mixed task roles [${uniqueTaskRoles.join(', ')}], falling back to ${agentType}`);
1821
1923
  }
1822
- if (workerLaunchMode === 'interactive' && !skipWorkerReadyWait && !initialPrompt) {
1924
+ const startupDirectOutcome = workerLaunchMode === 'interactive' && !initialPrompt
1925
+ ? await attemptStartupDirectTrigger({
1926
+ teamName: sanitized,
1927
+ config: config,
1928
+ workerName,
1929
+ workerIndex,
1930
+ paneId,
1931
+ workerCli: workerCliPlan[workerIndex - 1],
1932
+ inbox,
1933
+ triggerMessage: trigger,
1934
+ intent: triggerIntent,
1935
+ taskIds: workerTasks.map((task) => task.id),
1936
+ cwd: leaderCwd,
1937
+ timing: startupTiming,
1938
+ })
1939
+ : null;
1940
+ if (workerLaunchMode === 'interactive' && !skipWorkerReadyWait && !initialPrompt && !startupDirectOutcome?.ok) {
1941
+ startupTiming.mark('ready_wait_start', { worker: workerName, pane_id: paneId });
1823
1942
  const ready = await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId);
1943
+ startupTiming.mark('ready_wait_end', { worker: workerName, pane_id: paneId, ok: ready });
1824
1944
  if (!ready) {
1825
1945
  const workerAlive = isWorkerPaneOpen(sessionName, workerIndex, paneId);
1826
1946
  if (workerAlive) {
@@ -1841,10 +1961,11 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1841
1961
  };
1842
1962
  }
1843
1963
  }
1964
+ const startupReadyPromptObserved = workerLaunchMode === 'interactive' && !skipWorkerReadyWait && !initialPrompt;
1844
1965
  let dispatchOutcome = initialPrompt
1845
1966
  ? { ok: true, transport: 'none', reason: 'startup_prompt_delivered_at_launch' }
1846
- : { ok: false, transport: 'none', reason: 'not_attempted' };
1847
- if (!initialPrompt) {
1967
+ : (startupDirectOutcome ?? { ok: false, transport: 'none', reason: 'not_attempted' });
1968
+ if (!initialPrompt && !startupDirectOutcome?.ok) {
1848
1969
  for (let attempt = 1; attempt <= startupDispatchRetries; attempt++) {
1849
1970
  dispatchOutcome = await dispatchCriticalInboxInstruction({
1850
1971
  teamName: sanitized,
@@ -1861,7 +1982,20 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1861
1982
  inboxCorrelationKey: `startup:${workerName}`,
1862
1983
  requireWorkerStartupEvidence: true,
1863
1984
  startupEvidenceTimeoutMs: workerStartupEvidenceTimeoutMs,
1985
+ startupReadyPromptObserved,
1986
+ startupTiming,
1864
1987
  });
1988
+ await logStartupTiming({
1989
+ cwd: leaderCwd,
1990
+ teamName: sanitized,
1991
+ workerName,
1992
+ event: dispatchOutcome.ok ? 'startup_evidence' : 'startup_attempt_failed',
1993
+ paneId,
1994
+ elapsedMs: performance.now() - startupStartedAt,
1995
+ reason: dispatchOutcome.reason,
1996
+ requestId: dispatchOutcome.request_id,
1997
+ transport: dispatchOutcome.transport,
1998
+ }).catch(() => { });
1865
1999
  if (dispatchOutcome.ok)
1866
2000
  break;
1867
2001
  if (attempt < startupDispatchRetries) {
@@ -1895,6 +2029,16 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1895
2029
  });
1896
2030
  return { ok: true, workerIndex, workerName };
1897
2031
  }
2032
+ if (workerLaunchMode === 'prompt' && !workerAlive) {
2033
+ await recordPromptStartupWorkerStopped({
2034
+ teamName: sanitized,
2035
+ workerName,
2036
+ taskIds: workerTasks.map((task) => task.id),
2037
+ reason: dispatchOutcome.reason,
2038
+ cwd: leaderCwd,
2039
+ });
2040
+ return { ok: true, workerIndex, workerName };
2041
+ }
1898
2042
  return {
1899
2043
  ok: false,
1900
2044
  workerIndex,
@@ -1921,6 +2065,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
1921
2065
  throw firstStartupError.error;
1922
2066
  }
1923
2067
  await saveTeamConfig(config, leaderCwd);
2068
+ await startupTiming.flush();
1924
2069
  return {
1925
2070
  teamName: sanitized,
1926
2071
  sanitizedName: sanitized,
@@ -2046,7 +2191,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
2046
2191
  */
2047
2192
  export async function monitorTeam(teamName, cwd) {
2048
2193
  const monitorStartMs = performance.now();
2049
- const sanitized = sanitizeTeamName(teamName);
2194
+ const sanitized = resolveTeamNameForCurrentContext(teamName, cwd);
2050
2195
  const config = await readTeamConfig(sanitized, cwd);
2051
2196
  if (!config)
2052
2197
  return null;
@@ -2361,6 +2506,25 @@ export async function assignTask(teamName, workerName, taskId, cwd) {
2361
2506
  export async function reassignTask(teamName, taskId, _fromWorker, toWorker, cwd) {
2362
2507
  await assignTask(teamName, toWorker, taskId, cwd);
2363
2508
  }
2509
+ function resolveCommitHygieneArtifactTeamNames(config, internalTeamName) {
2510
+ const names = [];
2511
+ for (const value of [config.requested_name, config.display_name, internalTeamName]) {
2512
+ if (typeof value !== 'string' || value.trim() === '')
2513
+ continue;
2514
+ try {
2515
+ const sanitized = sanitizeTeamName(value);
2516
+ if (!names.includes(sanitized))
2517
+ names.push(sanitized);
2518
+ }
2519
+ catch {
2520
+ // Persisted display/request names are best-effort aliases. If an older
2521
+ // state file contains an invalid value, fall back to the internal name.
2522
+ }
2523
+ }
2524
+ if (!names.includes(internalTeamName))
2525
+ names.push(internalTeamName);
2526
+ return names;
2527
+ }
2364
2528
  /**
2365
2529
  * Graceful shutdown: send shutdown inbox to all workers, wait, force kill, cleanup.
2366
2530
  */
@@ -2368,7 +2532,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2368
2532
  const force = options.force === true;
2369
2533
  const confirmIssues = options.confirmIssues === true;
2370
2534
  let skipWorkerAcks = false;
2371
- const sanitized = sanitizeTeamName(teamName);
2535
+ const sanitized = resolveTeamNameForCurrentContext(teamName, cwd);
2372
2536
  const config = await readTeamConfig(sanitized, cwd);
2373
2537
  if (!config) {
2374
2538
  // No config -- just try to kill tmux session and clean up
@@ -2616,14 +2780,22 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2616
2780
  }
2617
2781
  }
2618
2782
  const artifactCwd = resolveTeamCommitHygieneArtifactCwd(config, cwd);
2619
- const ledger = await appendTeamCommitHygieneEntries(sanitized, commitHygieneEntries, artifactCwd);
2620
2783
  const taskView = await listTasks(sanitized, cwd).catch(() => []);
2621
- const commitHygieneContext = buildTeamCommitHygieneContext({
2622
- teamName: sanitized,
2623
- tasks: taskView,
2624
- ledger,
2625
- });
2626
- const commitHygieneArtifacts = await writeTeamCommitHygieneContext(sanitized, commitHygieneContext, artifactCwd);
2784
+ const internalLedger = await appendTeamCommitHygieneEntries(sanitized, commitHygieneEntries, artifactCwd);
2785
+ const commitHygieneArtifactTeamNames = resolveCommitHygieneArtifactTeamNames(config, sanitized);
2786
+ let commitHygieneArtifacts = null;
2787
+ for (const artifactTeamName of commitHygieneArtifactTeamNames) {
2788
+ const ledger = artifactTeamName === sanitized
2789
+ ? internalLedger
2790
+ : await appendTeamCommitHygieneEntries(artifactTeamName, internalLedger.entries, artifactCwd);
2791
+ const commitHygieneContext = buildTeamCommitHygieneContext({
2792
+ teamName: artifactTeamName,
2793
+ tasks: taskView,
2794
+ ledger,
2795
+ });
2796
+ const writtenArtifacts = await writeTeamCommitHygieneContext(artifactTeamName, commitHygieneContext, artifactCwd);
2797
+ commitHygieneArtifacts ??= writtenArtifacts;
2798
+ }
2627
2799
  // 5. Remove worker worktree-root instructions and team-scoped fallback instructions.
2628
2800
  for (const worker of config.workers) {
2629
2801
  if (!worker.worktree_path || !worker.team_state_root)
@@ -2675,7 +2847,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
2675
2847
  * Resume monitoring an existing team.
2676
2848
  */
2677
2849
  export async function resumeTeam(teamName, cwd) {
2678
- const sanitized = sanitizeTeamName(teamName);
2850
+ const sanitized = resolveTeamNameForCurrentContext(teamName, cwd);
2679
2851
  const config = await readTeamConfig(sanitized, cwd);
2680
2852
  if (!config)
2681
2853
  return null;
@@ -2951,8 +3123,89 @@ async function markDispatchRequestLeaderPaneMissingDeferred(params) {
2951
3123
  last_reason: 'leader_pane_missing_deferred',
2952
3124
  }, cwd).catch(() => { });
2953
3125
  }
3126
+ async function attemptStartupDirectTrigger(params) {
3127
+ const { teamName, config, workerName, workerIndex, paneId, workerCli, inbox, triggerMessage, intent, taskIds, cwd, timing, } = params;
3128
+ const safety = await evaluateStartupDirectTriggerSafety(config.tmux_session, workerIndex, paneId, workerCli);
3129
+ if (!safety.safe) {
3130
+ timing.mark('startup_direct_bypass', {
3131
+ worker: workerName,
3132
+ pane_id: paneId,
3133
+ ok: false,
3134
+ reason: `startup_direct_unsafe:${safety.reason}`,
3135
+ });
3136
+ return null;
3137
+ }
3138
+ const queued = await queueInboxInstruction({
3139
+ teamName,
3140
+ workerName,
3141
+ workerIndex,
3142
+ paneId,
3143
+ inbox,
3144
+ triggerMessage,
3145
+ intent,
3146
+ cwd,
3147
+ transportPreference: 'transport_direct',
3148
+ fallbackAllowed: false,
3149
+ inboxCorrelationKey: `startup-direct:${workerName}`,
3150
+ notify: (_target, message) => notifyWorkerOutcome(config, workerIndex, message, paneId),
3151
+ });
3152
+ timing.mark('dispatch_queued', {
3153
+ worker: workerName,
3154
+ pane_id: paneId,
3155
+ ok: queued.ok,
3156
+ reason: queued.reason,
3157
+ transport: queued.transport,
3158
+ request_id: queued.request_id,
3159
+ });
3160
+ timing.mark('direct_fallback', {
3161
+ worker: workerName,
3162
+ pane_id: paneId,
3163
+ ok: queued.ok,
3164
+ reason: queued.ok ? `startup_direct_trigger_sent:${safety.reason}` : queued.reason,
3165
+ transport: queued.transport,
3166
+ request_id: queued.request_id,
3167
+ });
3168
+ if (!queued.ok)
3169
+ return queued;
3170
+ const effectiveWorkerCli = workerCli ?? 'codex';
3171
+ const workerStartupEvidence = await waitForWorkerStartupEvidence({
3172
+ teamName,
3173
+ workerName,
3174
+ workerCli: effectiveWorkerCli,
3175
+ cwd,
3176
+ timeoutMs: 0,
3177
+ pollMs: STARTUP_EVIDENCE_POLL_MS,
3178
+ });
3179
+ timing.mark('startup_evidence', {
3180
+ worker: workerName,
3181
+ pane_id: paneId,
3182
+ ok: workerStartupEvidence !== 'none',
3183
+ reason: workerStartupEvidence,
3184
+ transport: queued.transport,
3185
+ request_id: queued.request_id,
3186
+ });
3187
+ const reason = workerStartupEvidence === 'none'
3188
+ ? `${effectiveWorkerCli}_startup_direct_no_evidence:${safety.reason}`
3189
+ : `startup_direct_trigger_sent:${safety.reason}`;
3190
+ if ((effectiveWorkerCli === 'codex' || effectiveWorkerCli === 'claude') && workerStartupEvidence === 'none') {
3191
+ await recordRecoverableStartupIssue({
3192
+ teamName,
3193
+ workerName,
3194
+ taskIds,
3195
+ reason,
3196
+ cwd,
3197
+ });
3198
+ }
3199
+ return {
3200
+ ...queued,
3201
+ reason,
3202
+ };
3203
+ }
2954
3204
  async function dispatchCriticalInboxInstruction(params) {
2955
- const { teamName, config, workerName, workerIndex, paneId, workerCli, inbox, triggerMessage, intent, cwd, dispatchPolicy, inboxCorrelationKey, requireWorkerStartupEvidence, startupEvidenceTimeoutMs, } = params;
3205
+ const { teamName, config, workerName, workerIndex, paneId, workerCli, inbox, triggerMessage, intent, cwd, dispatchPolicy, inboxCorrelationKey, requireWorkerStartupEvidence, startupEvidenceTimeoutMs, startupReadyPromptObserved = false, startupTiming, } = params;
3206
+ const noteTiming = (phase, details) => {
3207
+ startupTiming?.mark(phase, { worker: workerName, pane_id: paneId, ...details });
3208
+ };
2956
3209
  if (config.worker_launch_mode === 'prompt') {
2957
3210
  return await queueInboxInstruction({
2958
3211
  teamName,
@@ -2999,12 +3252,26 @@ async function dispatchCriticalInboxInstruction(params) {
2999
3252
  inboxCorrelationKey,
3000
3253
  notify: () => ({ ok: true, transport: 'hook', reason: 'queued_for_hook_dispatch' }),
3001
3254
  });
3255
+ noteTiming('dispatch_queued', {
3256
+ ok: queued.ok,
3257
+ reason: queued.reason,
3258
+ transport: queued.transport,
3259
+ request_id: queued.request_id,
3260
+ });
3002
3261
  if (!queued.request_id)
3003
3262
  return { ...queued, ok: false, reason: 'dispatch_request_missing_id' };
3004
3263
  const receipt = await waitForDispatchReceipt(teamName, queued.request_id, cwd, {
3005
3264
  timeoutMs: dispatchPolicy.dispatch_ack_timeout_ms,
3006
3265
  pollMs: 50,
3007
3266
  });
3267
+ if (receipt) {
3268
+ noteTiming('hook_receipt', {
3269
+ ok: receipt.status === 'delivered' || receipt.status === 'notified',
3270
+ reason: receipt.status,
3271
+ transport: 'hook',
3272
+ request_id: queued.request_id,
3273
+ });
3274
+ }
3008
3275
  if (receipt?.status === 'delivered') {
3009
3276
  return { ok: true, transport: 'hook', reason: 'hook_receipt_delivered', request_id: queued.request_id };
3010
3277
  }
@@ -3015,6 +3282,14 @@ async function dispatchCriticalInboxInstruction(params) {
3015
3282
  if (!requiresObservedStartupEvidence) {
3016
3283
  return { ok: true, transport: 'hook', reason: 'hook_receipt_notified', request_id: queued.request_id };
3017
3284
  }
3285
+ if (startupReadyPromptObserved) {
3286
+ return {
3287
+ ok: true,
3288
+ transport: 'hook',
3289
+ reason: 'hook_receipt_notified_with_ready_prompt',
3290
+ request_id: queued.request_id,
3291
+ };
3292
+ }
3018
3293
  startupEvidence = await waitForWorkerStartupEvidence({
3019
3294
  teamName,
3020
3295
  workerName,
@@ -3022,6 +3297,12 @@ async function dispatchCriticalInboxInstruction(params) {
3022
3297
  cwd,
3023
3298
  timeoutMs: startupEvidenceTimeoutMs,
3024
3299
  });
3300
+ noteTiming('startup_evidence', {
3301
+ ok: startupEvidence !== 'none',
3302
+ reason: startupEvidence,
3303
+ transport: 'hook',
3304
+ request_id: queued.request_id,
3305
+ });
3025
3306
  if (startupEvidence !== 'none') {
3026
3307
  return {
3027
3308
  ok: true,
@@ -3034,13 +3315,21 @@ async function dispatchCriticalInboxInstruction(params) {
3034
3315
  if (receipt?.status === 'failed') {
3035
3316
  const fallback = await notifyWorkerOutcome(config, workerIndex, triggerMessage, paneId);
3036
3317
  if (fallback.ok) {
3037
- const fallbackStartupEvidence = await waitForRequiredStartupEvidenceAfterDirectFallback({
3038
- requireWorkerStartupEvidence,
3039
- workerCli,
3040
- teamName,
3041
- workerName,
3042
- cwd,
3043
- timeoutMs: startupEvidenceTimeoutMs,
3318
+ const fallbackStartupEvidence = startupReadyPromptObserved
3319
+ ? 'ready_prompt'
3320
+ : await waitForRequiredStartupEvidenceAfterDirectFallback({
3321
+ requireWorkerStartupEvidence,
3322
+ workerCli,
3323
+ teamName,
3324
+ workerName,
3325
+ cwd,
3326
+ timeoutMs: startupEvidenceTimeoutMs,
3327
+ });
3328
+ noteTiming('startup_evidence', {
3329
+ ok: fallbackStartupEvidence !== 'none',
3330
+ reason: fallbackStartupEvidence,
3331
+ transport: fallback.transport,
3332
+ request_id: queued.request_id,
3044
3333
  });
3045
3334
  if (requiresObservedStartupEvidence && fallbackStartupEvidence === 'none') {
3046
3335
  await transitionDispatchRequest(teamName, queued.request_id, 'failed', 'failed', { last_reason: `${workerCli}_startup_no_evidence_after_fallback:${fallback.reason}` }, cwd).catch(() => { });
@@ -3071,6 +3360,12 @@ async function dispatchCriticalInboxInstruction(params) {
3071
3360
  };
3072
3361
  }
3073
3362
  const fallback = await notifyWorkerOutcome(config, workerIndex, triggerMessage, paneId);
3363
+ noteTiming('direct_fallback', {
3364
+ ok: fallback.ok,
3365
+ reason: fallback.reason,
3366
+ transport: fallback.transport,
3367
+ request_id: queued.request_id,
3368
+ });
3074
3369
  const startupFallbackLabel = receipt?.status === 'notified' && requiresObservedStartupEvidence
3075
3370
  ? `${workerCli}_startup_no_evidence`
3076
3371
  : null;
@@ -3078,13 +3373,21 @@ async function dispatchCriticalInboxInstruction(params) {
3078
3373
  ? `${startupFallbackLabel}_fallback_failed:${fallback.reason}`
3079
3374
  : `fallback_attempted_but_unconfirmed:${fallback.reason}`;
3080
3375
  if (fallback.ok) {
3081
- const fallbackStartupEvidence = await waitForRequiredStartupEvidenceAfterDirectFallback({
3082
- requireWorkerStartupEvidence,
3083
- workerCli,
3084
- teamName,
3085
- workerName,
3086
- cwd,
3087
- timeoutMs: startupEvidenceTimeoutMs,
3376
+ const fallbackStartupEvidence = startupReadyPromptObserved
3377
+ ? 'ready_prompt'
3378
+ : await waitForRequiredStartupEvidenceAfterDirectFallback({
3379
+ requireWorkerStartupEvidence,
3380
+ workerCli,
3381
+ teamName,
3382
+ workerName,
3383
+ cwd,
3384
+ timeoutMs: startupEvidenceTimeoutMs,
3385
+ });
3386
+ noteTiming('startup_evidence', {
3387
+ ok: fallbackStartupEvidence !== 'none',
3388
+ reason: fallbackStartupEvidence,
3389
+ transport: fallback.transport,
3390
+ request_id: queued.request_id,
3088
3391
  });
3089
3392
  if (requiresObservedStartupEvidence && fallbackStartupEvidence === 'none') {
3090
3393
  const current = await readDispatchRequest(teamName, queued.request_id, cwd);