oh-my-copilot 5.6.2 → 5.8.0

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 (264) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +2 -0
  4. package/bridge/__pycache__/gyoshu_bridge.cpython-312.pyc +0 -0
  5. package/bridge/claude-md-coordinator.cjs +2 -2
  6. package/bridge/cli.cjs +81405 -76559
  7. package/bridge/mcp-server.cjs +40048 -32187
  8. package/bridge/runtime-cli.cjs +943 -199
  9. package/bridge/team-mcp.cjs +716 -125
  10. package/bridge/team.js +1066 -290
  11. package/dist/__tests__/cli-win32-warning.test.js +7 -0
  12. package/dist/__tests__/cli-win32-warning.test.js.map +1 -1
  13. package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts +2 -0
  14. package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts.map +1 -0
  15. package/dist/__tests__/run-cjs-copilot-worker-routing.test.js +294 -0
  16. package/dist/__tests__/run-cjs-copilot-worker-routing.test.js.map +1 -0
  17. package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts +2 -0
  18. package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts.map +1 -0
  19. package/dist/__tests__/run-cjs-fail-closed-timeout.test.js +68 -0
  20. package/dist/__tests__/run-cjs-fail-closed-timeout.test.js.map +1 -0
  21. package/dist/__tests__/run-cjs-session-end-budget.test.d.ts +2 -0
  22. package/dist/__tests__/run-cjs-session-end-budget.test.d.ts.map +1 -0
  23. package/dist/__tests__/run-cjs-session-end-budget.test.js +171 -0
  24. package/dist/__tests__/run-cjs-session-end-budget.test.js.map +1 -0
  25. package/dist/__tests__/session-end-process-exit.test.js +70 -2
  26. package/dist/__tests__/session-end-process-exit.test.js.map +1 -1
  27. package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts +2 -0
  28. package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts.map +1 -0
  29. package/dist/cli/__tests__/doctor-conflicts-json.test.js +46 -0
  30. package/dist/cli/__tests__/doctor-conflicts-json.test.js.map +1 -0
  31. package/dist/cli/__tests__/team.test.js +67 -0
  32. package/dist/cli/__tests__/team.test.js.map +1 -1
  33. package/dist/cli/__tests__/wait-teleport-json.test.d.ts +2 -0
  34. package/dist/cli/__tests__/wait-teleport-json.test.d.ts.map +1 -0
  35. package/dist/cli/__tests__/wait-teleport-json.test.js +64 -0
  36. package/dist/cli/__tests__/wait-teleport-json.test.js.map +1 -0
  37. package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts +2 -0
  38. package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts.map +1 -0
  39. package/dist/cli/commands/__tests__/smoke-tier2.test.js +110 -0
  40. package/dist/cli/commands/__tests__/smoke-tier2.test.js.map +1 -0
  41. package/dist/cli/commands/__tests__/team-status-json.test.d.ts +2 -0
  42. package/dist/cli/commands/__tests__/team-status-json.test.d.ts.map +1 -0
  43. package/dist/cli/commands/__tests__/team-status-json.test.js +141 -0
  44. package/dist/cli/commands/__tests__/team-status-json.test.js.map +1 -0
  45. package/dist/cli/commands/factory.d.ts +2 -0
  46. package/dist/cli/commands/factory.d.ts.map +1 -1
  47. package/dist/cli/commands/factory.js +8 -1
  48. package/dist/cli/commands/factory.js.map +1 -1
  49. package/dist/cli/commands/smoke.d.ts +36 -0
  50. package/dist/cli/commands/smoke.d.ts.map +1 -0
  51. package/dist/cli/commands/smoke.js +169 -0
  52. package/dist/cli/commands/smoke.js.map +1 -0
  53. package/dist/cli/commands/team.d.ts +2 -0
  54. package/dist/cli/commands/team.d.ts.map +1 -1
  55. package/dist/cli/commands/team.js +124 -10
  56. package/dist/cli/commands/team.js.map +1 -1
  57. package/dist/cli/index.d.ts.map +1 -1
  58. package/dist/cli/index.js +70 -10
  59. package/dist/cli/index.js.map +1 -1
  60. package/dist/cli/win32-warning.d.ts +7 -1
  61. package/dist/cli/win32-warning.d.ts.map +1 -1
  62. package/dist/cli/win32-warning.js +9 -1
  63. package/dist/cli/win32-warning.js.map +1 -1
  64. package/dist/constants/names.d.ts +1 -0
  65. package/dist/constants/names.d.ts.map +1 -1
  66. package/dist/constants/names.js +1 -0
  67. package/dist/constants/names.js.map +1 -1
  68. package/dist/factory/__tests__/check-run.test.js +2 -2
  69. package/dist/factory/__tests__/check-run.test.js.map +1 -1
  70. package/dist/factory/__tests__/listener.test.js +1 -1
  71. package/dist/factory/__tests__/listener.test.js.map +1 -1
  72. package/dist/factory/__tests__/status.test.js +42 -0
  73. package/dist/factory/__tests__/status.test.js.map +1 -1
  74. package/dist/factory/listener.d.ts.map +1 -1
  75. package/dist/factory/listener.js +7 -8
  76. package/dist/factory/listener.js.map +1 -1
  77. package/dist/factory/status.d.ts +19 -1
  78. package/dist/factory/status.d.ts.map +1 -1
  79. package/dist/factory/status.js +49 -8
  80. package/dist/factory/status.js.map +1 -1
  81. package/dist/factory/watchdog.d.ts.map +1 -1
  82. package/dist/factory/watchdog.js +14 -0
  83. package/dist/factory/watchdog.js.map +1 -1
  84. package/dist/factory/watchdog.test.js +45 -0
  85. package/dist/factory/watchdog.test.js.map +1 -1
  86. package/dist/hooks/session-end/__tests__/action-runner.test.js +5 -0
  87. package/dist/hooks/session-end/__tests__/action-runner.test.js.map +1 -1
  88. package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js +186 -1
  89. package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js.map +1 -1
  90. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts +2 -0
  91. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts.map +1 -0
  92. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js +49 -0
  93. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js.map +1 -0
  94. package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js +30 -1
  95. package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js.map +1 -1
  96. package/dist/hooks/session-end/__tests__/spawn-next.test.js +104 -3
  97. package/dist/hooks/session-end/__tests__/spawn-next.test.js.map +1 -1
  98. package/dist/hooks/session-end/__tests__/worker.test.js +6 -0
  99. package/dist/hooks/session-end/__tests__/worker.test.js.map +1 -1
  100. package/dist/hooks/session-end/action-runner.d.ts.map +1 -1
  101. package/dist/hooks/session-end/action-runner.js +2 -1
  102. package/dist/hooks/session-end/action-runner.js.map +1 -1
  103. package/dist/hooks/session-end/chain-enqueuer.d.ts +70 -7
  104. package/dist/hooks/session-end/chain-enqueuer.d.ts.map +1 -1
  105. package/dist/hooks/session-end/chain-enqueuer.js +354 -82
  106. package/dist/hooks/session-end/chain-enqueuer.js.map +1 -1
  107. package/dist/hooks/session-end/foreground-bootstrap.d.ts.map +1 -1
  108. package/dist/hooks/session-end/foreground-bootstrap.js +24 -16
  109. package/dist/hooks/session-end/foreground-bootstrap.js.map +1 -1
  110. package/dist/hooks/session-end/index.d.ts +2 -3
  111. package/dist/hooks/session-end/index.d.ts.map +1 -1
  112. package/dist/hooks/session-end/index.js +11 -13
  113. package/dist/hooks/session-end/index.js.map +1 -1
  114. package/dist/hooks/session-end/spawn-next.d.ts +81 -4
  115. package/dist/hooks/session-end/spawn-next.d.ts.map +1 -1
  116. package/dist/hooks/session-end/spawn-next.js +141 -14
  117. package/dist/hooks/session-end/spawn-next.js.map +1 -1
  118. package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts +17 -0
  119. package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts.map +1 -0
  120. package/dist/hooks/session-end/wiki-foreground-bootstrap.js +20 -0
  121. package/dist/hooks/session-end/wiki-foreground-bootstrap.js.map +1 -0
  122. package/dist/hooks/session-end/worker.d.ts.map +1 -1
  123. package/dist/hooks/session-end/worker.js +11 -6
  124. package/dist/hooks/session-end/worker.js.map +1 -1
  125. package/dist/lib/__tests__/atomic-write.test.js +32 -1
  126. package/dist/lib/__tests__/atomic-write.test.js.map +1 -1
  127. package/dist/lib/__tests__/mode-state-lock.test.js +121 -12
  128. package/dist/lib/__tests__/mode-state-lock.test.js.map +1 -1
  129. package/dist/lib/atomic-write.d.ts +13 -3
  130. package/dist/lib/atomic-write.d.ts.map +1 -1
  131. package/dist/lib/atomic-write.js +29 -15
  132. package/dist/lib/atomic-write.js.map +1 -1
  133. package/dist/lib/mode-state-io.d.ts +2 -2
  134. package/dist/lib/mode-state-io.d.ts.map +1 -1
  135. package/dist/lib/mode-state-io.js +16 -10
  136. package/dist/lib/mode-state-io.js.map +1 -1
  137. package/dist/mcp/__tests__/host-smoke-tool.test.d.ts +2 -0
  138. package/dist/mcp/__tests__/host-smoke-tool.test.d.ts.map +1 -0
  139. package/dist/mcp/__tests__/host-smoke-tool.test.js +174 -0
  140. package/dist/mcp/__tests__/host-smoke-tool.test.js.map +1 -0
  141. package/dist/mcp/disable-tools.d.ts.map +1 -1
  142. package/dist/mcp/disable-tools.js +1 -0
  143. package/dist/mcp/disable-tools.js.map +1 -1
  144. package/dist/mcp/tool-registry.d.ts.map +1 -1
  145. package/dist/mcp/tool-registry.js +2 -0
  146. package/dist/mcp/tool-registry.js.map +1 -1
  147. package/dist/shared/types.d.ts +11 -0
  148. package/dist/shared/types.d.ts.map +1 -1
  149. package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts +2 -0
  150. package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts.map +1 -0
  151. package/dist/smoke/__tests__/copilot-chain-scenario.test.js +154 -0
  152. package/dist/smoke/__tests__/copilot-chain-scenario.test.js.map +1 -0
  153. package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts +2 -0
  154. package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts.map +1 -0
  155. package/dist/smoke/__tests__/copilot-sdk-driver.test.js +591 -0
  156. package/dist/smoke/__tests__/copilot-sdk-driver.test.js.map +1 -0
  157. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts +2 -0
  158. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts.map +1 -0
  159. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js +362 -0
  160. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js.map +1 -0
  161. package/dist/smoke/__tests__/copilot-session-eval.test.d.ts +2 -0
  162. package/dist/smoke/__tests__/copilot-session-eval.test.d.ts.map +1 -0
  163. package/dist/smoke/__tests__/copilot-session-eval.test.js +264 -0
  164. package/dist/smoke/__tests__/copilot-session-eval.test.js.map +1 -0
  165. package/dist/smoke/__tests__/copilot-smoke.test.d.ts +2 -0
  166. package/dist/smoke/__tests__/copilot-smoke.test.d.ts.map +1 -0
  167. package/dist/smoke/__tests__/copilot-smoke.test.js +482 -0
  168. package/dist/smoke/__tests__/copilot-smoke.test.js.map +1 -0
  169. package/dist/smoke/copilot-binary.d.ts +21 -0
  170. package/dist/smoke/copilot-binary.d.ts.map +1 -0
  171. package/dist/smoke/copilot-binary.js +46 -0
  172. package/dist/smoke/copilot-binary.js.map +1 -0
  173. package/dist/smoke/copilot-chain-scenario.d.ts +61 -0
  174. package/dist/smoke/copilot-chain-scenario.d.ts.map +1 -0
  175. package/dist/smoke/copilot-chain-scenario.js +253 -0
  176. package/dist/smoke/copilot-chain-scenario.js.map +1 -0
  177. package/dist/smoke/copilot-sdk-driver.d.ts +189 -0
  178. package/dist/smoke/copilot-sdk-driver.d.ts.map +1 -0
  179. package/dist/smoke/copilot-sdk-driver.js +637 -0
  180. package/dist/smoke/copilot-sdk-driver.js.map +1 -0
  181. package/dist/smoke/copilot-sdk-scenarios.d.ts +200 -0
  182. package/dist/smoke/copilot-sdk-scenarios.d.ts.map +1 -0
  183. package/dist/smoke/copilot-sdk-scenarios.js +620 -0
  184. package/dist/smoke/copilot-sdk-scenarios.js.map +1 -0
  185. package/dist/smoke/copilot-session-env.d.ts +75 -0
  186. package/dist/smoke/copilot-session-env.d.ts.map +1 -0
  187. package/dist/smoke/copilot-session-env.js +154 -0
  188. package/dist/smoke/copilot-session-env.js.map +1 -0
  189. package/dist/smoke/copilot-session-eval.d.ts +90 -0
  190. package/dist/smoke/copilot-session-eval.d.ts.map +1 -0
  191. package/dist/smoke/copilot-session-eval.js +323 -0
  192. package/dist/smoke/copilot-session-eval.js.map +1 -0
  193. package/dist/smoke/copilot-smoke.d.ts +166 -0
  194. package/dist/smoke/copilot-smoke.d.ts.map +1 -0
  195. package/dist/smoke/copilot-smoke.js +688 -0
  196. package/dist/smoke/copilot-smoke.js.map +1 -0
  197. package/dist/smoke/process-utils.d.ts +47 -0
  198. package/dist/smoke/process-utils.d.ts.map +1 -0
  199. package/dist/smoke/process-utils.js +99 -0
  200. package/dist/smoke/process-utils.js.map +1 -0
  201. package/dist/team/__tests__/runtime-v2.monitor.test.js +35 -0
  202. package/dist/team/__tests__/runtime-v2.monitor.test.js.map +1 -1
  203. package/dist/team/__tests__/scaling.test.js +13 -0
  204. package/dist/team/__tests__/scaling.test.js.map +1 -1
  205. package/dist/team/__tests__/sdk-transport.runtime.test.d.ts +2 -0
  206. package/dist/team/__tests__/sdk-transport.runtime.test.d.ts.map +1 -0
  207. package/dist/team/__tests__/sdk-transport.runtime.test.js +96 -0
  208. package/dist/team/__tests__/sdk-transport.runtime.test.js.map +1 -0
  209. package/dist/team/__tests__/sdk-transport.test.d.ts +2 -0
  210. package/dist/team/__tests__/sdk-transport.test.d.ts.map +1 -0
  211. package/dist/team/__tests__/sdk-transport.test.js +650 -0
  212. package/dist/team/__tests__/sdk-transport.test.js.map +1 -0
  213. package/dist/team/__tests__/tmux-session.pane-env.test.d.ts +2 -0
  214. package/dist/team/__tests__/tmux-session.pane-env.test.d.ts.map +1 -0
  215. package/dist/team/__tests__/tmux-session.pane-env.test.js +52 -0
  216. package/dist/team/__tests__/tmux-session.pane-env.test.js.map +1 -0
  217. package/dist/team/api-interop.d.ts.map +1 -1
  218. package/dist/team/api-interop.js +8 -0
  219. package/dist/team/api-interop.js.map +1 -1
  220. package/dist/team/runtime-v2.d.ts +33 -0
  221. package/dist/team/runtime-v2.d.ts.map +1 -1
  222. package/dist/team/runtime-v2.js +338 -24
  223. package/dist/team/runtime-v2.js.map +1 -1
  224. package/dist/team/scaling.d.ts.map +1 -1
  225. package/dist/team/scaling.js +5 -0
  226. package/dist/team/scaling.js.map +1 -1
  227. package/dist/team/sdk-host.d.ts +48 -0
  228. package/dist/team/sdk-host.d.ts.map +1 -0
  229. package/dist/team/sdk-host.js +604 -0
  230. package/dist/team/sdk-host.js.map +1 -0
  231. package/dist/team/sdk-policy.d.ts +79 -0
  232. package/dist/team/sdk-policy.d.ts.map +1 -0
  233. package/dist/team/sdk-policy.js +186 -0
  234. package/dist/team/sdk-policy.js.map +1 -0
  235. package/dist/team/sdk-transport.d.ts +164 -0
  236. package/dist/team/sdk-transport.d.ts.map +1 -0
  237. package/dist/team/sdk-transport.js +266 -0
  238. package/dist/team/sdk-transport.js.map +1 -0
  239. package/dist/team/tmux-session.d.ts +7 -0
  240. package/dist/team/tmux-session.d.ts.map +1 -1
  241. package/dist/team/tmux-session.js +80 -18
  242. package/dist/team/tmux-session.js.map +1 -1
  243. package/dist/team/types.d.ts +2 -0
  244. package/dist/team/types.d.ts.map +1 -1
  245. package/dist/team/types.js.map +1 -1
  246. package/dist/tools/host-smoke-tool.d.ts +34 -0
  247. package/dist/tools/host-smoke-tool.d.ts.map +1 -0
  248. package/dist/tools/host-smoke-tool.js +133 -0
  249. package/dist/tools/host-smoke-tool.js.map +1 -0
  250. package/docs/CLAUDE.md +1 -1
  251. package/docs/DEVELOPERS.md +122 -0
  252. package/docs/HOOKS.md +34 -2
  253. package/docs/MIGRATION.md +194 -3
  254. package/docs/REFERENCE.md +95 -1
  255. package/docs/TOOLS.md +32 -1
  256. package/package.json +11 -2
  257. package/plugin.json +1 -1
  258. package/scripts/dev/repro-state-lock.mjs +1 -1
  259. package/scripts/lib/atomic-write.mjs +4 -2
  260. package/scripts/lib/state-lock.mjs +21 -12
  261. package/scripts/run.cjs +141 -18
  262. package/scripts/session-start.mjs +13 -0
  263. package/scripts/wiki-session-end.mjs +2 -1
  264. package/templates/hooks/lib/atomic-write.mjs +4 -2
@@ -63,6 +63,11 @@ function workerInstructionStateRoot(cwd, teamName) {
63
63
  import { currentProcessStartIdentity, isProcessIdentityDead, publishOwnerEpoch, readLatestOwnerEpoch, requireOwnerFence, requireOwnerProcessIdentity } from './team-owner-epoch.js';
64
64
  import { waitForRecoveryGateRecord } from './worker-activation-gate.js';
65
65
  import { isWorkerLaunchAttemptCurrent, isWorkerLaunchAttemptAccepted, loadCurrentWorkerLaunchAttempt, loadWorkerLaunchAttempt, observeWorkerLaunchProvider, retireAndCleanupCurrentWorkerLaunchAttempt, withWorkerLaunchAttemptFence, } from './worker-launch-ack.js';
66
+ import { isSdkTarget, launchSdkHost, readSdkSession, ringDoorbell, sdkPaneId, sdkTeamTarget, stopSdkHost, SDK_HOST_STOP_FLOOR_MS, } from './sdk-transport.js';
67
+ import { toCopilotModelId } from './model-contract.js';
68
+ import { resolveShimTarget } from '../smoke/copilot-sdk-driver.js';
69
+ import { resolveDefaultPluginRoot } from '../smoke/copilot-session-env.js';
70
+ import { getCopilotConfigDir } from '../utils/config-dir.js';
66
71
  import { isProcessIdentityLive } from '../platform/process-utils.js';
67
72
  import { activateTeamInstanceUnderLock, assertTeamInstanceUnderLock, buildTeamInstancePendingConfig, createTeamInstanceBinding, disposeTeamInstanceUnderLock, releaseFailedStartupReservationUnderLock, reserveTeamInstanceUnderLock, retryTeamInstanceDisposal, TeamInstanceError, withTeamInstanceLifecycleLock, } from './team-instance.js';
68
73
  let runtimeOwnerRecoveryClient;
@@ -89,6 +94,10 @@ export async function recoverDeadWorkerV2(teamName, cwd, { workerName, requestId
89
94
  return { outcome: 'failed', committed: false, error: 'invalid_input', requestId, recoveryId: '', teamName, workerName,
90
95
  updatedAt: new Date().toISOString(), message: 'cwd, workerName, and requestId are required; timeoutMs must be an integer from 180000 through 300000.' };
91
96
  }
97
+ if (isSdkTarget((await readTeamConfig(teamName, cwd).catch(() => null))?.tmux_session)) {
98
+ return { outcome: 'failed', committed: false, error: 'invalid_input', requestId, recoveryId: '', teamName, workerName,
99
+ updatedAt: new Date().toISOString(), message: 'recovery_unsupported_transport: sdk workers are not recovered; shut the team down and start it again.' };
100
+ }
92
101
  let instanceId = expectedInstanceId;
93
102
  try {
94
103
  const requestReservationPath = absPath(cwd, TeamPaths.recoveryRequestPending(requestId));
@@ -461,6 +470,25 @@ export function resolveTaskAssignment(task, resolvedRouting, roleRoutingConfig,
461
470
  role: canonical,
462
471
  };
463
472
  }
473
+ /**
474
+ * Whether a resolved task role was EXPLICITLY assigned — directly on the
475
+ * task (`task.role`) or via `team.roleRouting[role]` config — as opposed to
476
+ * merely INFERRED from task text by `routeTaskToRole` inside
477
+ * {@link resolveTaskAssignment}. Mirrors that function's own
478
+ * `hasExplicitRole`/`hasConfigForRole` resolution order so the two never
479
+ * diverge.
480
+ *
481
+ * Used by the sdk-transport contract-role guard: an explicitly assigned
482
+ * reviewer-style role is rejected outright (the sdk transport cannot process
483
+ * its verdict file), while a merely inferred one has its role dropped so the
484
+ * worker runs as a plain executor-style worker instead of failing team
485
+ * startup over text the user never opted into.
486
+ */
487
+ export function isExplicitTaskRoleAssignment(task, roleRoutingConfig, role) {
488
+ const hasExplicitRole = typeof task.role === 'string' && task.role.length > 0;
489
+ const hasConfigForRole = !!getRoleRoutingSpec(roleRoutingConfig, role);
490
+ return hasExplicitRole || hasConfigForRole;
491
+ }
464
492
  function sanitizeTeamName(name) {
465
493
  const sanitized = name.toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 30);
466
494
  if (!sanitized)
@@ -476,14 +504,14 @@ function resolvePreflightBinaryPath(agentType) {
476
504
  // Helper: retain the original pane ownership binding
477
505
  // ---------------------------------------------------------------------------
478
506
  function tmuxServerIdentityForTarget(providerTarget, identity) {
479
- if (providerTarget.startsWith('cmux:'))
507
+ if (providerTarget.startsWith('cmux:') || isSdkTarget(providerTarget))
480
508
  return undefined;
481
509
  if (!isValidTmuxServerIdentity(identity))
482
510
  return undefined;
483
511
  return identity;
484
512
  }
485
513
  function requireTmuxServerIdentity(providerTarget, identity) {
486
- if (providerTarget.startsWith('cmux:'))
514
+ if (providerTarget.startsWith('cmux:') || isSdkTarget(providerTarget))
487
515
  return undefined;
488
516
  if (!isValidTmuxServerIdentity(identity))
489
517
  throw new Error('tmux_server_identity_missing');
@@ -494,6 +522,9 @@ function configuredPaneOwnership(config, worker) {
494
522
  const providerTarget = config.tmux_session;
495
523
  if (!paneId || !providerTarget)
496
524
  return null;
525
+ // SDK workers have no pane; their liveness is the host's provider record.
526
+ if (isSdkTarget(paneId) || isSdkTarget(providerTarget))
527
+ return null;
497
528
  const provider = paneId.startsWith('%') ? 'tmux' : 'cmux';
498
529
  const identity = provider === 'tmux'
499
530
  ? tmuxServerIdentityForTarget(providerTarget, config.tmux_server_identity)
@@ -1009,6 +1040,143 @@ async function readUnresolvedStartupLaunch(opts, paneId) {
1009
1040
  return null;
1010
1041
  }
1011
1042
  }
1043
+ export function resolveSdkTeamSettings(pluginCfg) {
1044
+ const sdk = pluginCfg.team?.sdk ?? {};
1045
+ const envNumber = (key) => {
1046
+ const n = Number(process.env[key]);
1047
+ return Number.isFinite(n) && n > 0 ? n : undefined;
1048
+ };
1049
+ // The runtime floors `--max-ai-credits` at its own minimum; the host enforces
1050
+ // this value itself, so a non-positive or non-numeric cap is a config error.
1051
+ const configCap = sdk.maxCreditsPerWorker;
1052
+ if (configCap !== undefined && !(typeof configCap === 'number' && Number.isFinite(configCap) && configCap > 0)) {
1053
+ throw new Error(`invalid_team_sdk_config:maxCreditsPerWorker must be a positive number (got ${JSON.stringify(configCap)})`);
1054
+ }
1055
+ const model = process.env.OMC_TEAM_SDK_MODEL?.trim() || sdk.model?.trim();
1056
+ return {
1057
+ maxCreditsPerWorker: envNumber('OMC_TEAM_SDK_MAX_CREDITS') ?? configCap ?? 10,
1058
+ ...(model ? { model } : {}),
1059
+ startupEvidenceMs: envNumber('OMC_TEAM_SDK_STARTUP_EVIDENCE_MS') ?? sdk.startupEvidenceMs ?? 180_000,
1060
+ denyTools: pluginCfg.permissions?.workerDenyTools ?? [],
1061
+ denyUrls: pluginCfg.permissions?.workerDenyUrls ?? [],
1062
+ };
1063
+ }
1064
+ /**
1065
+ * SDK counterpart of {@link spawnV2Worker}: no pane. The inbox, trigger text,
1066
+ * startup evidence gate (task claim bound to the launch attempt) and output
1067
+ * contract are the pane path's; only the provider launch (a detached
1068
+ * `omg team sdk-host`) and the delivery (the doorbell) differ.
1069
+ */
1070
+ async function spawnSdkV2Worker(opts, sdk) {
1071
+ const injectContract = shouldInjectContract(opts.role ?? null, opts.agentType);
1072
+ const outputFile = injectContract && opts.role
1073
+ ? cliWorkerOutputFilePath(teamStateRoot(opts.cwd, opts.teamName), opts.workerName, {
1074
+ taskId: opts.taskId,
1075
+ assignmentId: opts.verdictAssignmentId,
1076
+ })
1077
+ : undefined;
1078
+ const cliOutputContract = injectContract && opts.role && outputFile
1079
+ ? renderCliWorkerOutputContract(opts.role, outputFile)
1080
+ : undefined;
1081
+ const instruction = buildV2TaskInstruction(opts.teamName, opts.workerName, opts.task, opts.taskId, opts.agentType, cliOutputContract);
1082
+ const stateRoot = teamStateRoot(opts.cwd, opts.teamName);
1083
+ const startupBaseline = await captureWorkerStartupBaseline(opts.teamName, opts.workerName, opts.taskId, opts.cwd);
1084
+ const binary = opts.launchDescriptor.binary;
1085
+ const copilotBin = /\.(cmd|bat|ps1)$/i.test(binary) ? resolveShimTarget(binary) : binary;
1086
+ if (!copilotBin) {
1087
+ return { paneId: null, startupAssigned: false, startupFailureReason: `sdk_copilot_shim_unresolved:${binary}` };
1088
+ }
1089
+ const descriptorModel = opts.launchDescriptor.model ? toCopilotModelId(opts.launchDescriptor.model) : undefined;
1090
+ const model = sdk.model ?? descriptorModel;
1091
+ const workerCwd = opts.workerCwd ?? opts.cwd;
1092
+ const launch = await launchSdkHost({
1093
+ leaderCwd: opts.cwd,
1094
+ teamName: opts.teamName,
1095
+ workerName: opts.workerName,
1096
+ instanceId: opts.instanceId,
1097
+ workerCwd,
1098
+ stateRoot,
1099
+ workerEnv: {
1100
+ ...getModelWorkerEnv(opts.teamName, opts.workerName, opts.agentType, process.env, opts.role),
1101
+ OMC_TEAM_STATE_ROOT: stateRoot,
1102
+ OMC_TEAM_LEADER_CWD: opts.cwd,
1103
+ ...(opts.worktreePath ? { OMC_TEAM_WORKTREE_PATH: opts.worktreePath } : {}),
1104
+ OMC_TEAM_WORKER_CWD: workerCwd,
1105
+ },
1106
+ copilotBin,
1107
+ pluginRoot: resolveDefaultPluginRoot(),
1108
+ userConfigDir: getCopilotConfigDir(),
1109
+ ...(model ? { model } : {}),
1110
+ maxCredits: sdk.maxCreditsPerWorker,
1111
+ denyTools: sdk.denyTools,
1112
+ denyUrls: sdk.denyUrls,
1113
+ });
1114
+ const paneId = sdkPaneId(opts.workerName);
1115
+ if (!launch.ok) {
1116
+ return {
1117
+ paneId: launch.attempt ? paneId : null,
1118
+ startupAssigned: false,
1119
+ startupFailureReason: launch.reason,
1120
+ ...(launch.attempt ? { launchAttemptId: launch.attempt.attempt_id } : {}),
1121
+ };
1122
+ }
1123
+ const attempt = launch.attempt;
1124
+ const triggerMessage = `${generateTriggerMessage(opts.teamName, opts.workerName, workerInstructionStateRoot(opts.cwd, opts.teamName))} ` +
1125
+ `[launch:${attempt.attempt_id.slice(0, 12)}]`;
1126
+ const fenced = await withWorkerLaunchAttemptFence(attempt, () => queueInboxInstruction({
1127
+ teamName: opts.teamName,
1128
+ workerName: opts.workerName,
1129
+ workerIndex: opts.workerIndex + 1,
1130
+ paneId,
1131
+ inbox: instruction,
1132
+ triggerMessage,
1133
+ cwd: opts.cwd,
1134
+ transportPreference: 'prompt_stdin',
1135
+ fallbackAllowed: false,
1136
+ inboxCorrelationKey: `startup:${opts.workerName}:${opts.taskId}:${attempt.attempt_id}`,
1137
+ notify: async (_target, message) => {
1138
+ ringDoorbell(stateRoot, opts.workerName, { kind: 'prompt', text: message, from: 'leader-fixed' });
1139
+ const settled = await waitForStartupEvidenceBudget(async () => {
1140
+ if (await hasCurrentWorkerStartupEvidence(opts.teamName, opts.workerName, opts.taskId, opts.cwd, startupBaseline, attempt.attempt_id))
1141
+ return true;
1142
+ // A host that failed or died cannot produce evidence; stop waiting.
1143
+ const session = readSdkSession(stateRoot, opts.workerName);
1144
+ if (session?.state === 'failed' || session?.state === 'closed')
1145
+ throw new Error(`sdk_host_${session.state}:${session.last_error ?? ''}`);
1146
+ return false;
1147
+ }, sdk.startupEvidenceMs, 500).catch(() => false);
1148
+ return settled
1149
+ ? { ok: true, transport: 'prompt_stdin', reason: 'sdk_worker_startup_confirmed' }
1150
+ : { ok: false, transport: 'prompt_stdin', reason: 'copilot_sdk_startup_evidence_missing' };
1151
+ },
1152
+ deps: { writeWorkerInbox },
1153
+ }));
1154
+ const outcome = fenced.ok ? fenced.value : { ok: false, reason: 'worker_launch_attempt_superseded' };
1155
+ if (!outcome.ok) {
1156
+ const session = readSdkSession(stateRoot, opts.workerName);
1157
+ const claimError = session?.last_error ? redactBoundedDiagnostic(session.last_error, 240) : undefined;
1158
+ const stopped = await stopSdkHost(attempt, stateRoot, { timeoutMs: 30_000 });
1159
+ const cleaned = stopped && await retireAndCleanupCurrentWorkerLaunchAttempt(attempt, 'startup_dispatch_failed', async () => true).catch(() => false);
1160
+ if (!cleaned) {
1161
+ const error = new Error(`worker_startup_cleanup_unverified:${opts.workerName}:${paneId}`);
1162
+ error.unresolvedLaunch = { name: opts.workerName, paneId, launchAttemptId: attempt.attempt_id, provider: 'copilot' };
1163
+ throw error;
1164
+ }
1165
+ return {
1166
+ paneId,
1167
+ startupAssigned: false,
1168
+ startupFailureReason: outcome.reason,
1169
+ ...(claimError ? { claimError } : {}),
1170
+ launchAttemptId: attempt.attempt_id,
1171
+ };
1172
+ }
1173
+ return {
1174
+ paneId,
1175
+ startupAssigned: true,
1176
+ launchAttemptId: attempt.attempt_id,
1177
+ ...(outputFile ? { outputFile } : {}),
1178
+ };
1179
+ }
1012
1180
  /**
1013
1181
  * Spawn a single v2 worker in a tmux pane.
1014
1182
  * Writes CLI API inbox (no done.json), waits for ready, sends inbox path.
@@ -3142,7 +3310,13 @@ async function rollbackStartedNativeWorktreeStartup(args) {
3142
3310
  await writeStartedStartupRollbackEvidence({ ...args, reason: 'launch_attempt_identity_unverified', worker: worker.name });
3143
3311
  throw new Error(`worker_cleanup_incomplete:${worker.name}:launch_attempt_identity_unverified`);
3144
3312
  }
3313
+ if (isSdkTarget(worker.paneId) && !await stopSdkHost(attempt, teamStateRoot(args.cwd, args.teamName), { timeoutMs: 30_000 })) {
3314
+ await writeStartedStartupRollbackEvidence({ ...args, reason: 'sdk_host_stop_unverified', worker: worker.name });
3315
+ throw new Error(`worker_cleanup_incomplete:${worker.name}:sdk_host_stop_unverified`);
3316
+ }
3145
3317
  const cleaned = await retireAndCleanupCurrentWorkerLaunchAttempt(attempt, 'startup_rollback', async () => {
3318
+ if (isSdkTarget(worker.paneId))
3319
+ return true;
3146
3320
  const ownership = {
3147
3321
  provider: worker.paneId.startsWith('%') ? 'tmux' : 'cmux',
3148
3322
  providerTarget: args.sessionName,
@@ -3167,7 +3341,8 @@ async function rollbackStartedNativeWorktreeStartup(args) {
3167
3341
  throw new Error(`worker_cleanup_incomplete:${worker.name}:provider_cleanup_unverified`);
3168
3342
  }
3169
3343
  }
3170
- if (sessionCleanupRequired && !(args.workerPaneIds.length === 0 && args.sessionMode === 'split-pane' && (args.launchedWorkers?.length ?? 0) > 0)) {
3344
+ if (sessionCleanupRequired && !isSdkTarget(args.sessionName)
3345
+ && !(args.workerPaneIds.length === 0 && args.sessionMode === 'split-pane' && (args.launchedWorkers?.length ?? 0) > 0)) {
3171
3346
  const sessionCleaned = await killTeamSession(args.sessionName, args.workerPaneIds, args.leaderPaneId ?? undefined, {
3172
3347
  sessionMode: args.sessionMode,
3173
3348
  ...(args.tmuxServerIdentity ? { tmuxServerIdentity: args.tmuxServerIdentity } : {}),
@@ -3309,6 +3484,13 @@ export async function startTeamV2(config) {
3309
3484
  // do NOT change routing — user must recreate the team to pick up changes.
3310
3485
  const pluginCfg = config.pluginConfig ?? loadConfig();
3311
3486
  const resolvedRouting = buildResolvedRoutingSnapshot(pluginCfg);
3487
+ const transport = config.transport ?? pluginCfg.team?.transport ?? 'pane';
3488
+ if (transport !== 'pane' && transport !== 'sdk')
3489
+ throw new Error(`invalid_team_transport:${String(transport)}`);
3490
+ const sdkMode = transport === 'sdk';
3491
+ const sdkSettings = sdkMode ? resolveSdkTeamSettings(pluginCfg) : null;
3492
+ if (sdkMode && config.autoMerge)
3493
+ throw new Error('sdk_transport_unsupported:auto_merge');
3312
3494
  let worktreeMode = normalizeTeamWorktreeMode(process.env.OMC_TEAM_WORKTREE_MODE ?? pluginCfg.team?.ops?.worktreeMode);
3313
3495
  // Auto-merge gate (M5 + M3 hardening). Forces worktreeMode='named' so each
3314
3496
  // worker has a real branch the orchestrator can merge from.
@@ -3387,18 +3569,53 @@ export async function startTeamV2(config) {
3387
3569
  const workerName = workerNames[i];
3388
3570
  const taskIndex = startupByWorker.get(workerName);
3389
3571
  const fallbackAgent = (agentTypes[i % agentTypes.length] ?? agentTypes[0] ?? 'claude');
3390
- const resolvedAssignment = taskIndex === undefined
3572
+ const roleRoutingConfig = pluginCfg.team?.roleRouting;
3573
+ const task = taskIndex === undefined ? undefined : config.tasks[taskIndex];
3574
+ const resolvedAssignment = task === undefined
3391
3575
  ? { agentType: fallbackAgent, model: '', reasoningEffort: undefined, role: undefined }
3392
- : resolveTaskAssignment(config.tasks[taskIndex], resolvedRouting, pluginCfg.team?.roleRouting, fallbackAgent);
3576
+ : resolveTaskAssignment(task, resolvedRouting, roleRoutingConfig, fallbackAgent);
3577
+ let role = resolvedAssignment.role ?? undefined;
3578
+ // Under the sdk transport, a contract role (critic/code-reviewer/
3579
+ // security-reviewer/test-engineer) that was only INFERRED from task text
3580
+ // — not explicitly assigned via task.role or team.roleRouting config —
3581
+ // carries no user intent to honor by failing the whole team start. Drop
3582
+ // the role instead: the worker launches as a plain executor-style
3583
+ // worker (no verdict contract, no role-specific prompt override). An
3584
+ // explicitly assigned contract role is still rejected below.
3585
+ if (sdkMode && role && shouldInjectContract(role, resolvedAssignment.agentType)
3586
+ && task && !isExplicitTaskRoleAssignment(task, roleRoutingConfig, role)) {
3587
+ process.stderr.write(`[omg team] sdk transport: inferred role "${role}" for worker ${workerName} `
3588
+ + `would require a verdict contract the sdk transport cannot process; running as a plain executor `
3589
+ + `instead (an explicitly assigned contract role is still rejected)\n`);
3590
+ role = undefined;
3591
+ }
3393
3592
  const assignment = {
3394
3593
  agentType: resolvedAssignment.agentType,
3395
3594
  model: resolvedAssignment.model || resolveDefaultModel(resolvedAssignment.agentType),
3396
3595
  reasoningEffort: resolvedAssignment.reasoningEffort,
3397
- ...(resolvedAssignment.role ? { role: resolvedAssignment.role } : {}),
3596
+ ...(role ? { role } : {}),
3398
3597
  };
3399
3598
  startupAssignments.set(workerName, assignment);
3400
3599
  effectiveAgentTypes.add(assignment.agentType);
3401
3600
  }
3601
+ if (sdkMode && [...effectiveAgentTypes].some((agentType) => agentType !== 'copilot')) {
3602
+ throw new Error(`sdk_transport_requires_copilot_workers:${[...effectiveAgentTypes].join(',')}`);
3603
+ }
3604
+ if (sdkMode) {
3605
+ // Reviewer-style roles publish a verdict file the leader consumes only
3606
+ // after the provider exits (or, for cursor, on file appearance). An SDK
3607
+ // host never exits between turns, so its verdict would never be read.
3608
+ // Only an EXPLICITLY assigned contract role reaches this check — an
3609
+ // inferred one was already dropped above.
3610
+ const contractRoles = [...new Set([...startupAssignments.values()]
3611
+ .flatMap((a) => (a.role && shouldInjectContract(a.role, a.agentType) ? [a.role] : [])))];
3612
+ if (contractRoles.length > 0) {
3613
+ throw new Error(`sdk_transport_unsupported:contract_roles:${contractRoles.join(',')} `
3614
+ + `(${[...CONTRACT_ROLES].join('/')} workers emit verdict files the sdk transport does not process; `
3615
+ + `use --transport pane for an explicitly assigned reviewer-style role, or stop assigning it `
3616
+ + `explicitly to let it run as a plain executor)`);
3617
+ }
3618
+ }
3402
3619
  for (const agentType of effectiveAgentTypes) {
3403
3620
  try {
3404
3621
  resolvedBinaryPaths[agentType] = resolvePreflightBinaryPath(agentType).path;
@@ -3540,7 +3757,10 @@ export async function startTeamV2(config) {
3540
3757
  throw startupCleanupIncompleteError(error);
3541
3758
  throw error;
3542
3759
  }
3543
- for (const line of formatWorkerPermissionLines(preparedLaunches.values())) {
3760
+ const permissionLines = sdkSettings
3761
+ ? [`[omg team] copilot sdk workers (x${preparedLaunches.size}): no allow-all; host permission policy (writes: worktree + team state; shell: denylist, not a sandbox: all but team control/smoke/tmux/git push; no sub-agent tools); deny: ${[...sdkSettings.denyTools, ...sdkSettings.denyUrls.map((u) => `url(${u})`)].join(', ') || '(none)'}; credit cap ${sdkSettings.maxCreditsPerWorker}/worker`]
3762
+ : formatWorkerPermissionLines(preparedLaunches.values());
3763
+ for (const line of permissionLines) {
3544
3764
  process.stderr.write(`${line}\n`);
3545
3765
  }
3546
3766
  // Set up worker state dirs and overlays (with v2 CLI API instructions)
@@ -3578,9 +3798,12 @@ export async function startTeamV2(config) {
3578
3798
  // Create tmux session (leader only — workers spawned below)
3579
3799
  let session;
3580
3800
  try {
3581
- session = await createTeamSession(sanitized, 0, leaderCwd, {
3582
- newWindow: Boolean(config.newWindow),
3583
- });
3801
+ // SDK workers need no multiplexer: the team target only names the transport.
3802
+ session = sdkMode
3803
+ ? { sessionName: sdkTeamTarget(sanitized), leaderPaneId: '', workerPaneIds: [], sessionMode: 'split-pane' }
3804
+ : await createTeamSession(sanitized, 0, leaderCwd, {
3805
+ newWindow: Boolean(config.newWindow),
3806
+ });
3584
3807
  }
3585
3808
  catch (error) {
3586
3809
  if (!await rollbackBeforeConfig(error))
@@ -3603,6 +3826,7 @@ export async function startTeamV2(config) {
3603
3826
  ?? (agentTypes[i % agentTypes.length] ?? agentTypes[0] ?? 'claude'),
3604
3827
  worker_cli: preparedLaunches.get(wName).descriptor.provider,
3605
3828
  launch_descriptor: preparedLaunches.get(wName).descriptor,
3829
+ ...(sdkMode ? { transport: 'sdk' } : {}),
3606
3830
  assigned_tasks: [],
3607
3831
  working_dir: worktree?.path ?? leaderCwd,
3608
3832
  team_state_root: teamStateRoot(leaderCwd, sanitized),
@@ -3743,7 +3967,7 @@ export async function startTeamV2(config) {
3743
3967
  const workerInfo = workersInfo[workerIndex];
3744
3968
  if (!workerInfo)
3745
3969
  continue;
3746
- const workerLaunch = await spawnV2Worker({
3970
+ const spawnOptions = {
3747
3971
  sessionName,
3748
3972
  ...(session.tmuxServerIdentity ? { tmuxServerIdentity: session.tmuxServerIdentity } : {}),
3749
3973
  leaderPaneId,
@@ -3762,7 +3986,10 @@ export async function startTeamV2(config) {
3762
3986
  autoMerge: Boolean(config.autoMerge),
3763
3987
  ...(prepared.role ? { role: prepared.role } : {}),
3764
3988
  ...(prepared.verdictAssignmentId ? { verdictAssignmentId: prepared.verdictAssignmentId } : {}),
3765
- });
3989
+ };
3990
+ const workerLaunch = sdkSettings
3991
+ ? await spawnSdkV2Worker(spawnOptions, sdkSettings)
3992
+ : await spawnV2Worker(spawnOptions);
3766
3993
  if (workerLaunch.paneId) {
3767
3994
  if (workerLaunch.startupAssigned)
3768
3995
  workerPaneIds.push(workerLaunch.paneId);
@@ -4157,9 +4384,12 @@ async function processCliWorkerVerdictsUnderLock(teamName, cwd, expectedInstance
4157
4384
  if (!outputFile)
4158
4385
  continue;
4159
4386
  const paneOwnership = configuredPaneOwnership(config, worker);
4160
- const liveness = paneOwnership
4161
- ? await getOwnedWorkerLiveness(paneOwnership)
4162
- : 'unknown';
4387
+ // SDK workers: the post-exit contract keys on the host's provider record.
4388
+ const liveness = isSdkTarget(worker.pane_id)
4389
+ ? await getWorkerProviderLiveness(sanitized, cwd, config.instance_id, worker)
4390
+ : paneOwnership
4391
+ ? await getOwnedWorkerLiveness(paneOwnership)
4392
+ : 'unknown';
4163
4393
  const workerRole = normalizeDelegationRole(worker.role);
4164
4394
  const cursorReviewer = worker.worker_cli === 'cursor'
4165
4395
  && CONTRACT_ROLES.has(workerRole);
@@ -4789,19 +5019,28 @@ export async function monitorTeamV2(teamName, cwd, expectedInstanceId) {
4789
5019
  const recommendations = [];
4790
5020
  const workerScanStartMs = performance.now();
4791
5021
  const workerSignals = await Promise.all(config.workers.map(async (worker) => {
5022
+ const providerLiveness = await getWorkerProviderLiveness(sanitized, cwd, config.instance_id, worker);
5023
+ if (isSdkTarget(worker.pane_id)) {
5024
+ // No pane: liveness is the host's provider record, idleness its session file.
5025
+ const sdkSession = readSdkSession(teamStateRoot(cwd, sanitized), worker.name);
5026
+ const [status, heartbeat] = await Promise.all([
5027
+ readWorkerStatus(sanitized, worker.name, cwd),
5028
+ readWorkerHeartbeat(sanitized, worker.name, cwd),
5029
+ ]);
5030
+ return { worker, alive: providerLiveness === 'alive', liveness: providerLiveness, providerLiveness, status, heartbeat, paneCapture: '', sdkSession };
5031
+ }
4792
5032
  const ownership = configuredPaneOwnership(config, worker);
4793
5033
  const liveness = ownership ? await getOwnedWorkerLiveness(ownership) : 'unknown';
4794
- const providerLiveness = await getWorkerProviderLiveness(sanitized, cwd, config.instance_id, worker);
4795
5034
  const paneAlive = liveness === 'alive';
4796
5035
  const [status, heartbeat, paneCapture] = await Promise.all([
4797
5036
  readWorkerStatus(sanitized, worker.name, cwd),
4798
5037
  readWorkerHeartbeat(sanitized, worker.name, cwd),
4799
5038
  paneAlive && ownership ? captureOwnedTeamPane(ownership) : Promise.resolve(''),
4800
5039
  ]);
4801
- return { worker, alive: paneAlive, liveness, providerLiveness, status, heartbeat, paneCapture };
5040
+ return { worker, alive: paneAlive, liveness, providerLiveness, status, heartbeat, paneCapture, sdkSession: null };
4802
5041
  }));
4803
5042
  const workerScanMs = performance.now() - workerScanStartMs;
4804
- for (const { worker: w, alive, liveness, providerLiveness, status, heartbeat, paneCapture } of workerSignals) {
5043
+ for (const { worker: w, alive, liveness, providerLiveness, status, heartbeat, paneCapture, sdkSession } of workerSignals) {
4805
5044
  const currentTask = status.current_task_id ? taskById.get(status.current_task_id) ?? null : null;
4806
5045
  const outstandingTask = currentTask ?? findOutstandingWorkerTask(w, taskById, inProgressByOwner);
4807
5046
  const expectedTaskId = status.current_task_id ?? outstandingTask?.id ?? w.assigned_tasks[0] ?? '';
@@ -4833,6 +5072,18 @@ export async function monitorTeamV2(teamName, cwd, expectedInstanceId) {
4833
5072
  worktree_created: w.worktree_created,
4834
5073
  team_state_root: w.team_state_root,
4835
5074
  turnsWithoutProgress,
5075
+ ...(sdkSession ? { sdk: {
5076
+ state: sdkSession.state,
5077
+ turns: sdkSession.turns,
5078
+ queued: sdkSession.queued,
5079
+ last_event_type: sdkSession.last_event_type,
5080
+ last_event_at: sdkSession.last_event_at,
5081
+ last_error: sdkSession.last_error,
5082
+ session_id: sdkSession.session_id,
5083
+ model: sdkSession.model,
5084
+ credits: sdkSession.usage.credits,
5085
+ premium_requests: sdkSession.usage.shutdown_premium_requests ?? sdkSession.usage.premium_requests,
5086
+ } } : {}),
4836
5087
  });
4837
5088
  if (providerLiveness === 'dead') {
4838
5089
  deadWorkers.push(w.name);
@@ -4841,7 +5092,12 @@ export async function monitorTeamV2(teamName, cwd, expectedInstanceId) {
4841
5092
  recommendations.push(`Reassign task-${t.id} from dead ${w.name}`);
4842
5093
  }
4843
5094
  }
4844
- const paneSuggestsIdle = alive && paneLooksReady(paneCapture) && !paneHasActiveTask(paneCapture);
5095
+ // A capped SDK session is alive but takes no more prompts: idle-equivalent
5096
+ // for stall detection, and its outstanding work must move elsewhere.
5097
+ const sdkCapped = Boolean(sdkSession && providerLiveness !== 'dead' && sdkSession.state === 'capped');
5098
+ const paneSuggestsIdle = sdkSession
5099
+ ? alive && ((sdkSession.state === 'idle' && sdkSession.queued === 0) || sdkCapped)
5100
+ : alive && paneLooksReady(paneCapture) && !paneHasActiveTask(paneCapture);
4845
5101
  const statusFresh = isFreshTimestamp(status.updated_at);
4846
5102
  const heartbeatFresh = isFreshTimestamp(heartbeat?.last_turn_at);
4847
5103
  const hasWorkStartEvidence = expectedTaskId !== '' && hasWorkerStatusProgress(status, expectedTaskId);
@@ -4849,7 +5105,10 @@ export async function monitorTeamV2(teamName, cwd, expectedInstanceId) {
4849
5105
  ? getMissingDependencyIds(outstandingTask, taskById)
4850
5106
  : [];
4851
5107
  let stallReason = null;
4852
- if (paneSuggestsIdle && missingDependencyIds.length > 0) {
5108
+ if (sdkCapped) {
5109
+ stallReason = 'sdk_credit_cap';
5110
+ }
5111
+ else if (paneSuggestsIdle && missingDependencyIds.length > 0) {
4853
5112
  stallReason = 'missing_dependency';
4854
5113
  }
4855
5114
  else if (paneSuggestsIdle && expectedTaskId !== '' && !hasWorkStartEvidence) {
@@ -4863,7 +5122,16 @@ export async function monitorTeamV2(teamName, cwd, expectedInstanceId) {
4863
5122
  }
4864
5123
  if (stallReason) {
4865
5124
  nonReportingWorkers.push(w.name);
4866
- if (stallReason === 'missing_dependency') {
5125
+ if (stallReason === 'sdk_credit_cap') {
5126
+ const cappedTasks = inProgressByOwner.get(w.name) ?? [];
5127
+ for (const t of cappedTasks) {
5128
+ recommendations.push(`Reassign task-${t.id} from ${w.name}: sdk credit cap reached (${sdkSession?.last_error ?? 'capped'}); the worker takes no more prompts`);
5129
+ }
5130
+ if (cappedTasks.length === 0) {
5131
+ recommendations.push(`${w.name} reached its sdk credit cap and takes no more prompts; do not assign it new work`);
5132
+ }
5133
+ }
5134
+ else if (stallReason === 'missing_dependency') {
4867
5135
  recommendations.push(`Investigate ${w.name}: task-${outstandingTask?.id ?? expectedTaskId} is blocked by missing task ids [${missingDependencyIds.join(', ')}]; pane is idle at prompt`);
4868
5136
  }
4869
5137
  else if (stallReason === 'no_work_start_evidence') {
@@ -4980,6 +5248,11 @@ export async function cleanupStaleReservations(teamName, cwd) {
4980
5248
  'process_started_at' in owner)) {
4981
5249
  return; // Malformed owner, leave alone
4982
5250
  }
5251
+ // An active reservation is the running team's instance authority; its
5252
+ // owner is the `team start` CLI, which exits once the team is up. A dead
5253
+ // owner only makes a starting (never activated) reservation stale.
5254
+ if (reservation.phase === 'active')
5255
+ return;
4983
5256
  // Check if the process is dead
4984
5257
  const ownerRecord = owner;
4985
5258
  if (isProcessIdentityDead(ownerRecord)) {
@@ -5140,6 +5413,7 @@ export async function shutdownTeamV2(teamName, cwd, options = {}) {
5140
5413
  await assertTeamInstanceUnderLock(boundInstance);
5141
5414
  if (!current.config.tmux_session
5142
5415
  || (!current.config.tmux_session.startsWith('cmux:')
5416
+ && !isSdkTarget(current.config.tmux_session)
5143
5417
  && !isValidTmuxServerIdentity(current.config.tmux_server_identity))) {
5144
5418
  throw new Error('tmux_server_identity_missing');
5145
5419
  }
@@ -5329,6 +5603,9 @@ export async function shutdownTeamV2(teamName, cwd, options = {}) {
5329
5603
  const shutdownAckPath = `${shutdownRoot}/workers/${w.name}/shutdown-ack.json`;
5330
5604
  const shutdownInbox = `# Shutdown Request\n\nAll tasks are complete. Please wrap up and respond with a shutdown acknowledgement.\n\nWrite your ack to: ${shutdownAckPath}\nFormat: {"status":"accept","reason":"ok","updated_at":"<iso>"}\n\nThen exit your session.\n`;
5331
5605
  await writeWorkerInbox(sanitized, w.name, shutdownInbox, cwd);
5606
+ // An SDK host acks for its worker and tears its session down: no model turn is spent on shutdown.
5607
+ if (isSdkTarget(w.pane_id))
5608
+ ringDoorbell(teamStateRoot(cwd, sanitized), w.name, { kind: 'shutdown', from: 'leader-fixed' });
5332
5609
  }
5333
5610
  catch (err) {
5334
5611
  process.stderr.write(`[team/runtime-v2] shutdown request failed for ${w.name}: ${err}\n`);
@@ -5376,6 +5653,37 @@ export async function shutdownTeamV2(teamName, cwd, options = {}) {
5376
5653
  const providerCleanupFailures = [];
5377
5654
  const paneCleanupAlive = [];
5378
5655
  const paneCleanupUnknown = [];
5656
+ // SDK hosts stop in parallel within the caller's budget (session-end passes
5657
+ // 0): each already got the shutdown doorbell above, so the floor only lets a
5658
+ // host that is mid-teardown finish before the identity-checked kill.
5659
+ const sdkStopTimeoutMs = Math.max(timeoutMs, SDK_HOST_STOP_FLOOR_MS);
5660
+ const sdkStops = new Map();
5661
+ for (const worker of config.workers) {
5662
+ if (!isSdkTarget(worker.pane_id) || !worker.launch_attempt_id || !config.tmux_session)
5663
+ continue;
5664
+ const provider = worker.launch_descriptor?.provider ?? worker.worker_cli;
5665
+ if (!provider)
5666
+ continue;
5667
+ const attemptId = worker.launch_attempt_id;
5668
+ const paneId = worker.pane_id;
5669
+ sdkStops.set(worker.name, (async () => {
5670
+ const sdkAttempt = await loadWorkerLaunchAttempt({
5671
+ cwd,
5672
+ teamName: sanitized,
5673
+ instanceId: shutdownInstance.instance_id,
5674
+ workerName: worker.name,
5675
+ paneId,
5676
+ provider,
5677
+ attemptId,
5678
+ runtimeCliPath: resolveRuntimeCliPath(),
5679
+ });
5680
+ if (!sdkAttempt)
5681
+ return false;
5682
+ if (!await stopSdkHost(sdkAttempt, teamStateRoot(cwd, sanitized), { timeoutMs: sdkStopTimeoutMs }))
5683
+ return false;
5684
+ return retireAndCleanupCurrentWorkerLaunchAttempt(sdkAttempt, 'team_shutdown', async () => true);
5685
+ })().catch(() => false));
5686
+ }
5379
5687
  for (const worker of config.workers) {
5380
5688
  if (!worker.pane_id) {
5381
5689
  providerCleanupFailures.push(worker.name);
@@ -5393,6 +5701,11 @@ export async function shutdownTeamV2(teamName, cwd, options = {}) {
5393
5701
  providerCleanupFailures.push(worker.name);
5394
5702
  continue;
5395
5703
  }
5704
+ if (isSdkTarget(worker.pane_id)) {
5705
+ if (!await (sdkStops.get(worker.name) ?? Promise.resolve(false)))
5706
+ providerCleanupFailures.push(worker.name);
5707
+ continue;
5708
+ }
5396
5709
  const initialOwnership = configuredPaneOwnership(config, worker);
5397
5710
  const initialPaneLiveness = initialOwnership
5398
5711
  ? await getOwnedWorkerLiveness(initialOwnership)
@@ -5475,9 +5788,10 @@ export async function shutdownTeamV2(teamName, cwd, options = {}) {
5475
5788
  try {
5476
5789
  const { killTeamSession: killOwnedTeamSession, } = await import('./tmux-session.js');
5477
5790
  const ownsWindow = config.tmux_window_owned === true;
5478
- const workerPaneIds = recordedWorkerPaneIds;
5791
+ // SDK workers were stopped and retired above; there is no session or pane to close.
5792
+ const workerPaneIds = recordedWorkerPaneIds.filter((paneId) => !isSdkTarget(paneId));
5479
5793
  const splitPaneMode = Boolean(config.tmux_session && !ownsWindow && config.tmux_session.includes(':'));
5480
- if (!splitPaneMode && config.tmux_session) {
5794
+ if (!splitPaneMode && config.tmux_session && !isSdkTarget(config.tmux_session)) {
5481
5795
  if (!config.leader_pane_id) {
5482
5796
  if (!await rollbackShutdownForRetry())
5483
5797
  await finalizeAutoMerge();
@@ -5636,7 +5950,7 @@ export async function resumeTeamV2(teamName, cwd) {
5636
5950
  return null;
5637
5951
  // Verify tmux session is alive
5638
5952
  const sessionName = config.tmux_session || `omc-team-${sanitized}`;
5639
- if (sessionName.startsWith('cmux:')) {
5953
+ if (sessionName.startsWith('cmux:') || isSdkTarget(sessionName)) {
5640
5954
  return {
5641
5955
  teamName: sanitized,
5642
5956
  sanitizedName: sanitized,