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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -0
- package/bridge/__pycache__/gyoshu_bridge.cpython-312.pyc +0 -0
- package/bridge/claude-md-coordinator.cjs +2 -2
- package/bridge/cli.cjs +81405 -76559
- package/bridge/mcp-server.cjs +40048 -32187
- package/bridge/runtime-cli.cjs +943 -199
- package/bridge/team-mcp.cjs +716 -125
- package/bridge/team.js +1066 -290
- package/dist/__tests__/cli-win32-warning.test.js +7 -0
- package/dist/__tests__/cli-win32-warning.test.js.map +1 -1
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts +2 -0
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts.map +1 -0
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.js +294 -0
- package/dist/__tests__/run-cjs-copilot-worker-routing.test.js.map +1 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts +2 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts.map +1 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.js +68 -0
- package/dist/__tests__/run-cjs-fail-closed-timeout.test.js.map +1 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.d.ts +2 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.d.ts.map +1 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.js +171 -0
- package/dist/__tests__/run-cjs-session-end-budget.test.js.map +1 -0
- package/dist/__tests__/session-end-process-exit.test.js +70 -2
- package/dist/__tests__/session-end-process-exit.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-conflicts-json.test.js +46 -0
- package/dist/cli/__tests__/doctor-conflicts-json.test.js.map +1 -0
- package/dist/cli/__tests__/team.test.js +67 -0
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/wait-teleport-json.test.d.ts +2 -0
- package/dist/cli/__tests__/wait-teleport-json.test.d.ts.map +1 -0
- package/dist/cli/__tests__/wait-teleport-json.test.js +64 -0
- package/dist/cli/__tests__/wait-teleport-json.test.js.map +1 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts +2 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts.map +1 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.js +110 -0
- package/dist/cli/commands/__tests__/smoke-tier2.test.js.map +1 -0
- package/dist/cli/commands/__tests__/team-status-json.test.d.ts +2 -0
- package/dist/cli/commands/__tests__/team-status-json.test.d.ts.map +1 -0
- package/dist/cli/commands/__tests__/team-status-json.test.js +141 -0
- package/dist/cli/commands/__tests__/team-status-json.test.js.map +1 -0
- package/dist/cli/commands/factory.d.ts +2 -0
- package/dist/cli/commands/factory.d.ts.map +1 -1
- package/dist/cli/commands/factory.js +8 -1
- package/dist/cli/commands/factory.js.map +1 -1
- package/dist/cli/commands/smoke.d.ts +36 -0
- package/dist/cli/commands/smoke.d.ts.map +1 -0
- package/dist/cli/commands/smoke.js +169 -0
- package/dist/cli/commands/smoke.js.map +1 -0
- package/dist/cli/commands/team.d.ts +2 -0
- package/dist/cli/commands/team.d.ts.map +1 -1
- package/dist/cli/commands/team.js +124 -10
- package/dist/cli/commands/team.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +70 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/win32-warning.d.ts +7 -1
- package/dist/cli/win32-warning.d.ts.map +1 -1
- package/dist/cli/win32-warning.js +9 -1
- package/dist/cli/win32-warning.js.map +1 -1
- package/dist/constants/names.d.ts +1 -0
- package/dist/constants/names.d.ts.map +1 -1
- package/dist/constants/names.js +1 -0
- package/dist/constants/names.js.map +1 -1
- package/dist/factory/__tests__/check-run.test.js +2 -2
- package/dist/factory/__tests__/check-run.test.js.map +1 -1
- package/dist/factory/__tests__/listener.test.js +1 -1
- package/dist/factory/__tests__/listener.test.js.map +1 -1
- package/dist/factory/__tests__/status.test.js +42 -0
- package/dist/factory/__tests__/status.test.js.map +1 -1
- package/dist/factory/listener.d.ts.map +1 -1
- package/dist/factory/listener.js +7 -8
- package/dist/factory/listener.js.map +1 -1
- package/dist/factory/status.d.ts +19 -1
- package/dist/factory/status.d.ts.map +1 -1
- package/dist/factory/status.js +49 -8
- package/dist/factory/status.js.map +1 -1
- package/dist/factory/watchdog.d.ts.map +1 -1
- package/dist/factory/watchdog.js +14 -0
- package/dist/factory/watchdog.js.map +1 -1
- package/dist/factory/watchdog.test.js +45 -0
- package/dist/factory/watchdog.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/action-runner.test.js +5 -0
- package/dist/hooks/session-end/__tests__/action-runner.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js +186 -1
- package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts +2 -0
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts.map +1 -0
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js +49 -0
- package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js.map +1 -0
- package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js +30 -1
- package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/spawn-next.test.js +104 -3
- package/dist/hooks/session-end/__tests__/spawn-next.test.js.map +1 -1
- package/dist/hooks/session-end/__tests__/worker.test.js +6 -0
- package/dist/hooks/session-end/__tests__/worker.test.js.map +1 -1
- package/dist/hooks/session-end/action-runner.d.ts.map +1 -1
- package/dist/hooks/session-end/action-runner.js +2 -1
- package/dist/hooks/session-end/action-runner.js.map +1 -1
- package/dist/hooks/session-end/chain-enqueuer.d.ts +70 -7
- package/dist/hooks/session-end/chain-enqueuer.d.ts.map +1 -1
- package/dist/hooks/session-end/chain-enqueuer.js +354 -82
- package/dist/hooks/session-end/chain-enqueuer.js.map +1 -1
- package/dist/hooks/session-end/foreground-bootstrap.d.ts.map +1 -1
- package/dist/hooks/session-end/foreground-bootstrap.js +24 -16
- package/dist/hooks/session-end/foreground-bootstrap.js.map +1 -1
- package/dist/hooks/session-end/index.d.ts +2 -3
- package/dist/hooks/session-end/index.d.ts.map +1 -1
- package/dist/hooks/session-end/index.js +11 -13
- package/dist/hooks/session-end/index.js.map +1 -1
- package/dist/hooks/session-end/spawn-next.d.ts +81 -4
- package/dist/hooks/session-end/spawn-next.d.ts.map +1 -1
- package/dist/hooks/session-end/spawn-next.js +141 -14
- package/dist/hooks/session-end/spawn-next.js.map +1 -1
- package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts +17 -0
- package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts.map +1 -0
- package/dist/hooks/session-end/wiki-foreground-bootstrap.js +20 -0
- package/dist/hooks/session-end/wiki-foreground-bootstrap.js.map +1 -0
- package/dist/hooks/session-end/worker.d.ts.map +1 -1
- package/dist/hooks/session-end/worker.js +11 -6
- package/dist/hooks/session-end/worker.js.map +1 -1
- package/dist/lib/__tests__/atomic-write.test.js +32 -1
- package/dist/lib/__tests__/atomic-write.test.js.map +1 -1
- package/dist/lib/__tests__/mode-state-lock.test.js +121 -12
- package/dist/lib/__tests__/mode-state-lock.test.js.map +1 -1
- package/dist/lib/atomic-write.d.ts +13 -3
- package/dist/lib/atomic-write.d.ts.map +1 -1
- package/dist/lib/atomic-write.js +29 -15
- package/dist/lib/atomic-write.js.map +1 -1
- package/dist/lib/mode-state-io.d.ts +2 -2
- package/dist/lib/mode-state-io.d.ts.map +1 -1
- package/dist/lib/mode-state-io.js +16 -10
- package/dist/lib/mode-state-io.js.map +1 -1
- package/dist/mcp/__tests__/host-smoke-tool.test.d.ts +2 -0
- package/dist/mcp/__tests__/host-smoke-tool.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/host-smoke-tool.test.js +174 -0
- package/dist/mcp/__tests__/host-smoke-tool.test.js.map +1 -0
- package/dist/mcp/disable-tools.d.ts.map +1 -1
- package/dist/mcp/disable-tools.js +1 -0
- package/dist/mcp/disable-tools.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +2 -0
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/shared/types.d.ts +11 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-chain-scenario.test.js +154 -0
- package/dist/smoke/__tests__/copilot-chain-scenario.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.js +591 -0
- package/dist/smoke/__tests__/copilot-sdk-driver.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js +362 -0
- package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.js +264 -0
- package/dist/smoke/__tests__/copilot-session-eval.test.js.map +1 -0
- package/dist/smoke/__tests__/copilot-smoke.test.d.ts +2 -0
- package/dist/smoke/__tests__/copilot-smoke.test.d.ts.map +1 -0
- package/dist/smoke/__tests__/copilot-smoke.test.js +482 -0
- package/dist/smoke/__tests__/copilot-smoke.test.js.map +1 -0
- package/dist/smoke/copilot-binary.d.ts +21 -0
- package/dist/smoke/copilot-binary.d.ts.map +1 -0
- package/dist/smoke/copilot-binary.js +46 -0
- package/dist/smoke/copilot-binary.js.map +1 -0
- package/dist/smoke/copilot-chain-scenario.d.ts +61 -0
- package/dist/smoke/copilot-chain-scenario.d.ts.map +1 -0
- package/dist/smoke/copilot-chain-scenario.js +253 -0
- package/dist/smoke/copilot-chain-scenario.js.map +1 -0
- package/dist/smoke/copilot-sdk-driver.d.ts +189 -0
- package/dist/smoke/copilot-sdk-driver.d.ts.map +1 -0
- package/dist/smoke/copilot-sdk-driver.js +637 -0
- package/dist/smoke/copilot-sdk-driver.js.map +1 -0
- package/dist/smoke/copilot-sdk-scenarios.d.ts +200 -0
- package/dist/smoke/copilot-sdk-scenarios.d.ts.map +1 -0
- package/dist/smoke/copilot-sdk-scenarios.js +620 -0
- package/dist/smoke/copilot-sdk-scenarios.js.map +1 -0
- package/dist/smoke/copilot-session-env.d.ts +75 -0
- package/dist/smoke/copilot-session-env.d.ts.map +1 -0
- package/dist/smoke/copilot-session-env.js +154 -0
- package/dist/smoke/copilot-session-env.js.map +1 -0
- package/dist/smoke/copilot-session-eval.d.ts +90 -0
- package/dist/smoke/copilot-session-eval.d.ts.map +1 -0
- package/dist/smoke/copilot-session-eval.js +323 -0
- package/dist/smoke/copilot-session-eval.js.map +1 -0
- package/dist/smoke/copilot-smoke.d.ts +166 -0
- package/dist/smoke/copilot-smoke.d.ts.map +1 -0
- package/dist/smoke/copilot-smoke.js +688 -0
- package/dist/smoke/copilot-smoke.js.map +1 -0
- package/dist/smoke/process-utils.d.ts +47 -0
- package/dist/smoke/process-utils.d.ts.map +1 -0
- package/dist/smoke/process-utils.js +99 -0
- package/dist/smoke/process-utils.js.map +1 -0
- package/dist/team/__tests__/runtime-v2.monitor.test.js +35 -0
- package/dist/team/__tests__/runtime-v2.monitor.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +13 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/sdk-transport.runtime.test.d.ts +2 -0
- package/dist/team/__tests__/sdk-transport.runtime.test.d.ts.map +1 -0
- package/dist/team/__tests__/sdk-transport.runtime.test.js +96 -0
- package/dist/team/__tests__/sdk-transport.runtime.test.js.map +1 -0
- package/dist/team/__tests__/sdk-transport.test.d.ts +2 -0
- package/dist/team/__tests__/sdk-transport.test.d.ts.map +1 -0
- package/dist/team/__tests__/sdk-transport.test.js +650 -0
- package/dist/team/__tests__/sdk-transport.test.js.map +1 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.js +52 -0
- package/dist/team/__tests__/tmux-session.pane-env.test.js.map +1 -0
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +8 -0
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/runtime-v2.d.ts +33 -0
- package/dist/team/runtime-v2.d.ts.map +1 -1
- package/dist/team/runtime-v2.js +338 -24
- package/dist/team/runtime-v2.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +5 -0
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/sdk-host.d.ts +48 -0
- package/dist/team/sdk-host.d.ts.map +1 -0
- package/dist/team/sdk-host.js +604 -0
- package/dist/team/sdk-host.js.map +1 -0
- package/dist/team/sdk-policy.d.ts +79 -0
- package/dist/team/sdk-policy.d.ts.map +1 -0
- package/dist/team/sdk-policy.js +186 -0
- package/dist/team/sdk-policy.js.map +1 -0
- package/dist/team/sdk-transport.d.ts +164 -0
- package/dist/team/sdk-transport.d.ts.map +1 -0
- package/dist/team/sdk-transport.js +266 -0
- package/dist/team/sdk-transport.js.map +1 -0
- package/dist/team/tmux-session.d.ts +7 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +80 -18
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/types.d.ts +2 -0
- package/dist/team/types.d.ts.map +1 -1
- package/dist/team/types.js.map +1 -1
- package/dist/tools/host-smoke-tool.d.ts +34 -0
- package/dist/tools/host-smoke-tool.d.ts.map +1 -0
- package/dist/tools/host-smoke-tool.js +133 -0
- package/dist/tools/host-smoke-tool.js.map +1 -0
- package/docs/CLAUDE.md +1 -1
- package/docs/DEVELOPERS.md +122 -0
- package/docs/HOOKS.md +34 -2
- package/docs/MIGRATION.md +194 -3
- package/docs/REFERENCE.md +95 -1
- package/docs/TOOLS.md +32 -1
- package/package.json +11 -2
- package/plugin.json +1 -1
- package/scripts/dev/repro-state-lock.mjs +1 -1
- package/scripts/lib/atomic-write.mjs +4 -2
- package/scripts/lib/state-lock.mjs +21 -12
- package/scripts/run.cjs +141 -18
- package/scripts/session-start.mjs +13 -0
- package/scripts/wiki-session-end.mjs +2 -1
- package/templates/hooks/lib/atomic-write.mjs +4 -2
package/dist/team/runtime-v2.js
CHANGED
|
@@ -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.
|
|
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
|
|
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(
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
3582
|
-
|
|
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
|
|
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
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
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
|
-
|
|
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 (
|
|
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 === '
|
|
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
|
-
|
|
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,
|