oh-my-codex 0.8.11 → 0.8.13

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 (162) hide show
  1. package/README.md +43 -35
  2. package/dist/agents/__tests__/definitions.test.js +1 -0
  3. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  4. package/dist/agents/definitions.d.ts.map +1 -1
  5. package/dist/agents/definitions.js +11 -0
  6. package/dist/agents/definitions.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts +2 -0
  8. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts.map +1 -0
  9. package/dist/cli/__tests__/doctor-invalid-config.test.js +52 -0
  10. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -0
  11. package/dist/cli/__tests__/index.test.js +35 -3
  12. package/dist/cli/__tests__/index.test.js.map +1 -1
  13. package/dist/cli/__tests__/launch-fallback.test.d.ts +2 -0
  14. package/dist/cli/__tests__/launch-fallback.test.d.ts.map +1 -0
  15. package/dist/cli/__tests__/launch-fallback.test.js +60 -0
  16. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -0
  17. package/dist/cli/__tests__/resume.test.d.ts +2 -0
  18. package/dist/cli/__tests__/resume.test.d.ts.map +1 -0
  19. package/dist/cli/__tests__/resume.test.js +78 -0
  20. package/dist/cli/__tests__/resume.test.js.map +1 -0
  21. package/dist/cli/__tests__/session-search-help.test.d.ts +2 -0
  22. package/dist/cli/__tests__/session-search-help.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/session-search-help.test.js +36 -0
  24. package/dist/cli/__tests__/session-search-help.test.js.map +1 -0
  25. package/dist/cli/__tests__/session-search.test.d.ts +2 -0
  26. package/dist/cli/__tests__/session-search.test.d.ts.map +1 -0
  27. package/dist/cli/__tests__/session-search.test.js +77 -0
  28. package/dist/cli/__tests__/session-search.test.js.map +1 -0
  29. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +2 -0
  30. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  31. package/dist/cli/__tests__/team-decompose.test.js +41 -15
  32. package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
  33. package/dist/cli/__tests__/team.test.js +208 -3
  34. package/dist/cli/__tests__/team.test.js.map +1 -1
  35. package/dist/cli/doctor.d.ts.map +1 -1
  36. package/dist/cli/doctor.js +26 -0
  37. package/dist/cli/doctor.js.map +1 -1
  38. package/dist/cli/index.d.ts +4 -3
  39. package/dist/cli/index.d.ts.map +1 -1
  40. package/dist/cli/index.js +73 -27
  41. package/dist/cli/index.js.map +1 -1
  42. package/dist/cli/session-search.d.ts +8 -0
  43. package/dist/cli/session-search.d.ts.map +1 -0
  44. package/dist/cli/session-search.js +133 -0
  45. package/dist/cli/session-search.js.map +1 -0
  46. package/dist/cli/team.d.ts +13 -12
  47. package/dist/cli/team.d.ts.map +1 -1
  48. package/dist/cli/team.js +123 -39
  49. package/dist/cli/team.js.map +1 -1
  50. package/dist/hooks/__tests__/agents-overlay.test.js +33 -1
  51. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  52. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +219 -0
  53. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  54. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
  55. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  56. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +64 -1
  57. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  58. package/dist/hooks/__tests__/notify-hook-modules.test.js +7 -0
  59. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  60. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -1
  61. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  62. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +420 -5
  63. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  64. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +95 -0
  65. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  66. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +3 -0
  67. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  68. package/dist/hooks/__tests__/tmux-hook-engine.test.js +39 -1
  69. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  70. package/dist/hooks/agents-overlay.d.ts +6 -1
  71. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  72. package/dist/hooks/agents-overlay.js +45 -4
  73. package/dist/hooks/agents-overlay.js.map +1 -1
  74. package/dist/mcp/team-server.js +1 -1
  75. package/dist/mcp/team-server.js.map +1 -1
  76. package/dist/session-history/__tests__/search.test.d.ts +2 -0
  77. package/dist/session-history/__tests__/search.test.d.ts.map +1 -0
  78. package/dist/session-history/__tests__/search.test.js +150 -0
  79. package/dist/session-history/__tests__/search.test.js.map +1 -0
  80. package/dist/session-history/search.d.ts +31 -0
  81. package/dist/session-history/search.d.ts.map +1 -0
  82. package/dist/session-history/search.js +326 -0
  83. package/dist/session-history/search.js.map +1 -0
  84. package/dist/team/__tests__/allocation-policy.test.d.ts +2 -0
  85. package/dist/team/__tests__/allocation-policy.test.d.ts.map +1 -0
  86. package/dist/team/__tests__/allocation-policy.test.js +39 -0
  87. package/dist/team/__tests__/allocation-policy.test.js.map +1 -0
  88. package/dist/team/__tests__/api-interop.test.js +140 -4
  89. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  90. package/dist/team/__tests__/followup-planner.test.js +12 -0
  91. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  92. package/dist/team/__tests__/idle-nudge.test.js +6 -1
  93. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  94. package/dist/team/__tests__/rebalance-policy.test.d.ts +2 -0
  95. package/dist/team/__tests__/rebalance-policy.test.d.ts.map +1 -0
  96. package/dist/team/__tests__/rebalance-policy.test.js +125 -0
  97. package/dist/team/__tests__/rebalance-policy.test.js.map +1 -0
  98. package/dist/team/__tests__/runtime.test.js +315 -12
  99. package/dist/team/__tests__/runtime.test.js.map +1 -1
  100. package/dist/team/__tests__/state.test.js +20 -1
  101. package/dist/team/__tests__/state.test.js.map +1 -1
  102. package/dist/team/__tests__/team-ops-contract.test.js +1 -0
  103. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  104. package/dist/team/__tests__/worker-bootstrap.test.js +20 -3
  105. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  106. package/dist/team/allocation-policy.d.ts +23 -0
  107. package/dist/team/allocation-policy.d.ts.map +1 -0
  108. package/dist/team/allocation-policy.js +71 -0
  109. package/dist/team/allocation-policy.js.map +1 -0
  110. package/dist/team/api-interop.d.ts +1 -1
  111. package/dist/team/api-interop.d.ts.map +1 -1
  112. package/dist/team/api-interop.js +159 -0
  113. package/dist/team/api-interop.js.map +1 -1
  114. package/dist/team/idle-nudge.js +1 -1
  115. package/dist/team/idle-nudge.js.map +1 -1
  116. package/dist/team/rebalance-policy.d.ts +19 -0
  117. package/dist/team/rebalance-policy.d.ts.map +1 -0
  118. package/dist/team/rebalance-policy.js +48 -0
  119. package/dist/team/rebalance-policy.js.map +1 -0
  120. package/dist/team/runtime.d.ts.map +1 -1
  121. package/dist/team/runtime.js +132 -17
  122. package/dist/team/runtime.js.map +1 -1
  123. package/dist/team/state/types.d.ts +3 -0
  124. package/dist/team/state/types.d.ts.map +1 -1
  125. package/dist/team/state/types.js.map +1 -1
  126. package/dist/team/state.d.ts +8 -0
  127. package/dist/team/state.d.ts.map +1 -1
  128. package/dist/team/state.js +28 -12
  129. package/dist/team/state.js.map +1 -1
  130. package/dist/team/team-ops.d.ts +2 -1
  131. package/dist/team/team-ops.d.ts.map +1 -1
  132. package/dist/team/team-ops.js +1 -0
  133. package/dist/team/team-ops.js.map +1 -1
  134. package/dist/team/tmux-session.d.ts +5 -4
  135. package/dist/team/tmux-session.d.ts.map +1 -1
  136. package/dist/team/tmux-session.js +5 -67
  137. package/dist/team/tmux-session.js.map +1 -1
  138. package/dist/team/worker-bootstrap.d.ts +1 -0
  139. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  140. package/dist/team/worker-bootstrap.js +9 -2
  141. package/dist/team/worker-bootstrap.js.map +1 -1
  142. package/package.json +2 -1
  143. package/prompts/team-executor.md +57 -0
  144. package/prompts/team-orchestrator.md +8 -0
  145. package/scripts/notify-fallback-watcher.js +295 -1
  146. package/scripts/notify-hook/auto-nudge.js +20 -4
  147. package/scripts/notify-hook/team-dispatch.js +11 -58
  148. package/scripts/notify-hook/team-leader-nudge.js +59 -12
  149. package/scripts/notify-hook/team-tmux-guard.js +28 -11
  150. package/scripts/notify-hook/team-worker.js +3 -1
  151. package/scripts/notify-hook/tmux-injection.js +12 -13
  152. package/scripts/tmux-hook-engine.js +56 -0
  153. package/skills/team/SKILL.md +14 -0
  154. package/templates/catalog-manifest.json +5 -0
  155. package/dist/rtk/__tests__/index.test.d.ts +0 -2
  156. package/dist/rtk/__tests__/index.test.d.ts.map +0 -1
  157. package/dist/rtk/__tests__/index.test.js +0 -104
  158. package/dist/rtk/__tests__/index.test.js.map +0 -1
  159. package/dist/rtk/index.d.ts +0 -130
  160. package/dist/rtk/index.d.ts.map +0 -1
  161. package/dist/rtk/index.js +0 -257
  162. package/dist/rtk/index.js.map +0 -1
@@ -4,9 +4,9 @@ import { readdir, readFile } from 'fs/promises';
4
4
  import { performance } from 'perf_hooks';
5
5
  import { spawn } from 'child_process';
6
6
  import { sanitizeTeamName, isTmuxAvailable, createTeamSession, buildWorkerProcessLaunchSpec, resolveTeamWorkerCli, resolveTeamWorkerCliPlan, resolveTeamWorkerLaunchMode, waitForWorkerReady, dismissTrustPromptIfPresent, sleepFractionalSeconds, sendToWorker, sendToLeaderPane, sendToWorkerStdin, isWorkerAlive, getWorkerPanePid, killWorkerByPaneIdAsync, teardownWorkerPanes, unregisterResizeHook, destroyTeamSession, listTeamSessions, } 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, teamReadManifest as readTeamManifestV2, 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, } from './team-ops.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, 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, } from './team-ops.js';
8
8
  import { queueInboxInstruction, queueDirectMailboxMessage, queueBroadcastMailboxMessage, waitForDispatchReceipt, } from './mcp-comm.js';
9
- import { generateWorkerOverlay, writeTeamWorkerInstructionsFile, removeTeamWorkerInstructionsFile, generateInitialInbox, generateTaskAssignmentInbox, generateShutdownInbox, generateTriggerMessage, generateMailboxTriggerMessage, writeWorkerRoleInstructionsFile, } from './worker-bootstrap.js';
9
+ import { generateWorkerOverlay, writeTeamWorkerInstructionsFile, removeTeamWorkerInstructionsFile, generateInitialInbox, generateTaskAssignmentInbox, generateShutdownInbox, generateTriggerMessage, generateMailboxTriggerMessage, generateLeaderMailboxTriggerMessage, writeWorkerRoleInstructionsFile, } from './worker-bootstrap.js';
10
10
  import { loadRolePrompt } from './role-router.js';
11
11
  import { codexPromptsDir } from '../utils/paths.js';
12
12
  import { isLowComplexityAgentType, resolveTeamWorkerLaunchArgs, TEAM_LOW_COMPLEXITY_DEFAULT_MODEL, resolveTeamLowComplexityDefaultModel, parseTeamWorkerLaunchArgs, splitWorkerLaunchArgs, resolveAgentReasoningEffort, } from './model-contract.js';
@@ -14,6 +14,7 @@ import { resolveCanonicalTeamStateRoot } from './state-root.js';
14
14
  import { inferPhaseTargetFromTaskCounts, reconcilePhaseStateForMonitor } from './phase-controller.js';
15
15
  import { getTeamTmuxSessions } from '../notifications/tmux.js';
16
16
  import { hasStructuredVerificationEvidence } from '../verification/verifier.js';
17
+ import { buildRebalanceDecisions } from './rebalance-policy.js';
17
18
  import { readModeState, updateModeState } from '../modes/base.js';
18
19
  import { ensureWorktree, planWorktreeTarget, rollbackProvisionedWorktrees, } from './worktree.js';
19
20
  async function syncRootTeamModeStateOnTerminalPhase(teamName, phase, cwd) {
@@ -46,6 +47,46 @@ async function syncRootTeamModeStateOnTerminalPhase(teamName, phase, cwd) {
46
47
  // Best-effort compatibility sync only.
47
48
  }
48
49
  }
50
+ async function syncLinkedRalphModeStateOnTerminalPhase(teamName, phase, cwd, nowIso = new Date().toISOString()) {
51
+ if (phase !== 'complete' && phase !== 'failed' && phase !== 'cancelled')
52
+ return;
53
+ try {
54
+ const [teamState, ralphState] = await Promise.all([
55
+ readModeState('team', cwd),
56
+ readModeState('ralph', cwd),
57
+ ]);
58
+ if (!teamState || !ralphState)
59
+ return;
60
+ const stateTeamName = typeof teamState.team_name === 'string' ? teamState.team_name.trim() : '';
61
+ if (stateTeamName && stateTeamName !== teamName)
62
+ return;
63
+ if (teamState.linked_ralph !== true || ralphState.linked_team !== true)
64
+ return;
65
+ const terminalAt = typeof teamState.completed_at === 'string' && teamState.completed_at
66
+ ? teamState.completed_at
67
+ : nowIso;
68
+ const alreadySynced = ralphState.active === false
69
+ && ralphState.current_phase === phase
70
+ && ralphState.linked_team_terminal_phase === phase
71
+ && ralphState.linked_team_terminal_at === terminalAt
72
+ && ralphState.completed_at === terminalAt;
73
+ if (alreadySynced)
74
+ return;
75
+ await updateModeState('ralph', {
76
+ active: false,
77
+ current_phase: phase,
78
+ linked_mode: 'team',
79
+ linked_team: true,
80
+ linked_team_terminal_phase: phase,
81
+ linked_team_terminal_at: terminalAt,
82
+ completed_at: terminalAt,
83
+ last_turn_at: nowIso,
84
+ }, cwd);
85
+ }
86
+ catch {
87
+ // Best-effort compatibility sync only.
88
+ }
89
+ }
49
90
  function collectProvisionedShutdownWorktrees(config) {
50
91
  const seenWorktreePaths = new Set();
51
92
  const worktrees = [];
@@ -96,6 +137,43 @@ function resolveWorkerReadyTimeoutMs(env) {
96
137
  return parsed;
97
138
  return 45_000;
98
139
  }
140
+ function parseTeamWorkerContext(raw) {
141
+ if (typeof raw !== 'string' || raw.trim() === '')
142
+ return null;
143
+ const [teamName, workerName] = raw.trim().split('/');
144
+ if (!teamName || !workerName)
145
+ return null;
146
+ return { teamName, workerName };
147
+ }
148
+ function resolveManifestLookupCwds(cwd) {
149
+ const candidates = new Set([resolve(cwd)]);
150
+ const leaderCwd = process.env[TEAM_LEADER_CWD_ENV];
151
+ if (typeof leaderCwd === 'string' && leaderCwd.trim() !== '') {
152
+ candidates.add(resolve(leaderCwd));
153
+ }
154
+ const teamStateRoot = process.env[TEAM_STATE_ROOT_ENV];
155
+ if (typeof teamStateRoot === 'string' && teamStateRoot.trim() !== '') {
156
+ candidates.add(resolve(teamStateRoot, '..', '..'));
157
+ }
158
+ return [...candidates];
159
+ }
160
+ function resolveGovernancePolicy(governance, legacyPolicy) {
161
+ return normalizeTeamGovernance(governance, legacyPolicy);
162
+ }
163
+ async function assertNestedTeamAllowed(cwd) {
164
+ const workerContext = parseTeamWorkerContext(process.env.OMX_TEAM_WORKER);
165
+ if (!workerContext)
166
+ return;
167
+ for (const candidateCwd of resolveManifestLookupCwds(cwd)) {
168
+ const manifest = await readTeamManifestV2(workerContext.teamName, candidateCwd);
169
+ const governance = resolveGovernancePolicy(manifest?.governance, manifest?.policy);
170
+ if (governance.nested_teams_allowed)
171
+ return;
172
+ if (manifest)
173
+ break;
174
+ }
175
+ throw new Error('nested_team_disallowed');
176
+ }
99
177
  async function readWorkerStartupEvidence(teamName, workerName, cwd) {
100
178
  const status = await readWorkerStatus(teamName, workerName, cwd);
101
179
  if (typeof status.current_task_id === 'string' && status.current_task_id.trim() !== '') {
@@ -360,9 +438,8 @@ function resolveEffectiveWorkerCliForStartupLog(resolvedLaunchArgs, env) {
360
438
  * Start a new team: init state, create tmux session, bootstrap workers.
361
439
  */
362
440
  export async function startTeam(teamName, task, agentType, workerCount, tasks, cwd, options = {}) {
363
- if (process.env.OMX_TEAM_WORKER) {
364
- throw new Error('nested_team_disallowed');
365
- }
441
+ const leaderCwd = resolve(cwd);
442
+ await assertNestedTeamAllowed(leaderCwd);
366
443
  const workerLaunchMode = resolveTeamWorkerLaunchMode(process.env);
367
444
  const displayMode = workerLaunchMode === 'interactive' ? 'split_pane' : 'auto';
368
445
  if (workerLaunchMode === 'interactive') {
@@ -373,7 +450,6 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
373
450
  throw new Error('Team mode requires running inside tmux current leader pane');
374
451
  }
375
452
  }
376
- const leaderCwd = resolve(cwd);
377
453
  const sanitized = sanitizeTeamName(teamName);
378
454
  const teamStateRoot = resolveCanonicalTeamStateRoot(leaderCwd);
379
455
  const activeWorktreeMode = options.worktreeMode?.enabled
@@ -802,7 +878,7 @@ export async function monitorTeam(teamName, cwd) {
802
878
  if (reclaimed.ok && reclaimed.reclaimed)
803
879
  reclaimedTaskIds.push(task.id);
804
880
  }
805
- const taskView = reclaimedTaskIds.length > 0 ? await listTasks(sanitized, cwd) : allTasks;
881
+ let taskView = reclaimedTaskIds.length > 0 ? await listTasks(sanitized, cwd) : allTasks;
806
882
  const taskById = new Map(taskView.map((task) => [task.id, task]));
807
883
  const inProgressByOwner = new Map();
808
884
  for (const task of taskView) {
@@ -863,6 +939,39 @@ export async function monitorTeam(teamName, cwd) {
863
939
  recommendations.push(`Send reminder to non-reporting ${w.name}`);
864
940
  }
865
941
  }
942
+ for (const taskId of reclaimedTaskIds) {
943
+ recommendations.push(`Reclaimed expired claim for task-${taskId}`);
944
+ }
945
+ const rebalanceDecisions = buildRebalanceDecisions({
946
+ tasks: taskView,
947
+ workers: workers.map((worker) => ({
948
+ name: worker.name,
949
+ role: config.workers.find((entry) => entry.name === worker.name)?.role,
950
+ alive: worker.alive,
951
+ status: worker.status,
952
+ })),
953
+ reclaimedTaskIds,
954
+ });
955
+ let assignedDuringMonitor = false;
956
+ for (const decision of rebalanceDecisions) {
957
+ if (decision.type === 'assign' && decision.taskId && decision.workerName) {
958
+ try {
959
+ await assignTask(sanitized, decision.workerName, decision.taskId, cwd);
960
+ recommendations.push(`Assigned task-${decision.taskId} to ${decision.workerName}: ${decision.reason}`);
961
+ assignedDuringMonitor = true;
962
+ }
963
+ catch (error) {
964
+ const message = error instanceof Error ? error.message : String(error);
965
+ recommendations.push(`Unable to assign task-${decision.taskId} to ${decision.workerName}: ${message}`);
966
+ }
967
+ }
968
+ else {
969
+ recommendations.push(decision.reason);
970
+ }
971
+ }
972
+ if (assignedDuringMonitor) {
973
+ taskView = await listTasks(sanitized, cwd);
974
+ }
866
975
  // Count tasks
867
976
  const taskCounts = {
868
977
  total: taskView.length,
@@ -895,9 +1004,7 @@ export async function monitorTeam(teamName, cwd) {
895
1004
  await writeTeamPhaseState(sanitized, phaseState, cwd);
896
1005
  const phase = phaseState.current_phase;
897
1006
  await syncRootTeamModeStateOnTerminalPhase(sanitized, phase, cwd);
898
- for (const taskId of reclaimedTaskIds) {
899
- recommendations.push(`Reclaimed expired claim for task-${taskId}`);
900
- }
1007
+ await syncLinkedRalphModeStateOnTerminalPhase(sanitized, phase, cwd);
901
1008
  if (deadWorkerStall) {
902
1009
  recommendations.push('All workers are dead while work remains; mark the team failed or restart with fresh workers.');
903
1010
  }
@@ -969,10 +1076,11 @@ export async function assignTask(teamName, workerName, taskId, cwd) {
969
1076
  if (!task)
970
1077
  throw new Error(`Task ${taskId} not found`);
971
1078
  const manifest = await readTeamManifestV2(sanitized, cwd);
972
- if (manifest?.policy?.delegation_only && workerName === 'leader-fixed') {
1079
+ const governance = resolveGovernancePolicy(manifest?.governance, manifest?.policy);
1080
+ if (governance.delegation_only && workerName === 'leader-fixed') {
973
1081
  throw new Error('delegation_only_violation');
974
1082
  }
975
- if (manifest?.policy?.plan_approval_required && task.requires_code_change === true) {
1083
+ if (governance.plan_approval_required && task.requires_code_change === true) {
976
1084
  const approved = await isTaskApprovedForExecution(sanitized, taskId, cwd);
977
1085
  if (!approved) {
978
1086
  throw new Error('plan_approval_required');
@@ -1073,6 +1181,8 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1073
1181
  restoreTeamModelInstructionsFile(sanitized);
1074
1182
  return;
1075
1183
  }
1184
+ const manifest = await readTeamManifestV2(sanitized, cwd);
1185
+ const governance = resolveGovernancePolicy(manifest?.governance, manifest?.policy);
1076
1186
  if (!force) {
1077
1187
  const allTasks = await listTasks(sanitized, cwd);
1078
1188
  const gate = {
@@ -1084,11 +1194,12 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1084
1194
  failed: allTasks.filter((t) => t.status === 'failed').length,
1085
1195
  allowed: false,
1086
1196
  };
1087
- gate.allowed = gate.pending === 0 && gate.blocked === 0 && gate.in_progress === 0 && gate.failed === 0;
1197
+ gate.allowed = governance.cleanup_requires_all_workers_inactive !== true
1198
+ || (gate.pending === 0 && gate.blocked === 0 && gate.in_progress === 0 && gate.failed === 0);
1088
1199
  await appendTeamEvent(sanitized, {
1089
1200
  type: 'shutdown_gate',
1090
1201
  worker: 'leader-fixed',
1091
- reason: `allowed=${gate.allowed} total=${gate.total} pending=${gate.pending} blocked=${gate.blocked} in_progress=${gate.in_progress} completed=${gate.completed} failed=${gate.failed}${ralph ? ' policy=ralph' : ''}`,
1202
+ reason: `allowed=${gate.allowed} total=${gate.total} pending=${gate.pending} blocked=${gate.blocked} in_progress=${gate.in_progress} completed=${gate.completed} failed=${gate.failed} cleanup_requires_all_workers_inactive=${governance.cleanup_requires_all_workers_inactive}${ralph ? ' policy=ralph' : ''}`,
1092
1203
  }, cwd).catch(() => { });
1093
1204
  if (!gate.allowed) {
1094
1205
  const hasActiveWork = gate.pending > 0 || gate.blocked > 0 || gate.in_progress > 0;
@@ -1114,7 +1225,6 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1114
1225
  }, cwd).catch(() => { });
1115
1226
  }
1116
1227
  const sessionName = config.tmux_session;
1117
- const manifest = await readTeamManifestV2(sanitized, cwd);
1118
1228
  const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
1119
1229
  const shutdownRequestTimes = new Map();
1120
1230
  // 1. Send shutdown inbox to each worker
@@ -1209,6 +1319,9 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1209
1319
  leaderPaneId,
1210
1320
  hudPaneId,
1211
1321
  });
1322
+ if (hudPaneId) {
1323
+ await killWorkerByPaneIdAsync(hudPaneId, leaderPaneId ?? undefined);
1324
+ }
1212
1325
  // 4. Destroy tmux session
1213
1326
  if (!sessionName.includes(':')) {
1214
1327
  try {
@@ -1250,6 +1363,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
1250
1363
  worker: 'leader-fixed',
1251
1364
  reason: `total=${finalTasks.length} completed=${completed} failed=${failed} pending=${pending} force=${force}`,
1252
1365
  }, cwd).catch(() => { });
1366
+ await syncLinkedRalphModeStateOnTerminalPhase(sanitized, 'cancelled', cwd);
1253
1367
  }
1254
1368
  const cleanupErrors = [];
1255
1369
  const provisionedWorktrees = collectProvisionedShutdownWorktrees(config);
@@ -1331,7 +1445,8 @@ async function findActiveTeams(cwd, leaderSessionId) {
1331
1445
  const teamName = e.name;
1332
1446
  const cfg = await readTeamConfig(teamName, cwd);
1333
1447
  const manifest = await readTeamManifestV2(teamName, cwd);
1334
- if (manifest?.policy?.one_team_per_leader_session === false)
1448
+ const governance = resolveGovernancePolicy(manifest?.governance, manifest?.policy);
1449
+ if (governance.one_team_per_leader_session === false)
1335
1450
  continue;
1336
1451
  const workerLaunchMode = cfg?.worker_launch_mode
1337
1452
  ?? manifest?.policy?.worker_launch_mode
@@ -1807,7 +1922,7 @@ export async function sendWorkerMessage(teamName, fromWorker, toWorker, body, cw
1807
1922
  const manifest = await readTeamManifestV2(sanitized, cwd);
1808
1923
  const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
1809
1924
  if (toWorker === 'leader-fixed') {
1810
- const leaderTriggerMessage = `Team ${sanitized}: new worker message for leader from ${fromWorker}`;
1925
+ const leaderTriggerMessage = generateLeaderMailboxTriggerMessage(sanitized, fromWorker);
1811
1926
  const leaderTransportPreference = dispatchPolicy.dispatch_mode === 'transport_direct'
1812
1927
  ? 'transport_direct'
1813
1928
  : 'hook_preferred_with_fallback';