@playdrop/playdrop-cli 0.10.6 → 0.10.7

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.
@@ -35,6 +35,11 @@ exports.assertWorkerProjectVersionBumped = assertWorkerProjectVersionBumped;
35
35
  exports.buildAgentFailureCode = buildAgentFailureCode;
36
36
  exports.describeAgentFailureForEvent = describeAgentFailureForEvent;
37
37
  exports.createLocalPlaydropShim = createLocalPlaydropShim;
38
+ exports.readActivePersonalWorkerRuntimeState = readActivePersonalWorkerRuntimeState;
39
+ exports.parseLaunchAgentProgramArguments = parseLaunchAgentProgramArguments;
40
+ exports.assertLaunchAgentPlistCompatible = assertLaunchAgentPlistCompatible;
41
+ exports.showWorkerStatus = showWorkerStatus;
42
+ exports.setupWorker = setupWorker;
38
43
  exports.startWorker = startWorker;
39
44
  exports.reportTask = reportTask;
40
45
  exports.reportCatalogueTask = reportCatalogueTask;
@@ -59,6 +64,7 @@ const output_1 = require("../output");
59
64
  const shellProbe_1 = require("../shellProbe");
60
65
  const upload_1 = require("./upload");
61
66
  const review_1 = require("./review");
67
+ const agents_1 = require("./agents");
62
68
  const runtime_1 = require("./worker/runtime");
63
69
  var runtime_2 = require("./worker/runtime");
64
70
  Object.defineProperty(exports, "DEFAULT_CODEX_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CODEX_TIMEOUT_MS; } });
@@ -83,6 +89,7 @@ const CLAIM_BACKOFF_JITTER_MS = 500;
83
89
  const FAIL_REPORT_RETRY_DELAY_MS = 2000;
84
90
  const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
85
91
  const SLACK_API_BASE_URL = 'https://slack.com/api';
92
+ const WORKER_WRAPPER_CONTRACT_VERSION = 1;
86
93
  const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'GAME_UPDATE', 'GAME_REVIEW'];
87
94
  const requireFromWorker = (0, node_module_1.createRequire)(__filename);
88
95
  const PLAYDROP_PLUGIN_DAEMON_GAME_CREATION_SKILL_PATH = 'skills/daemon-game-creation/SKILL.md';
@@ -209,7 +216,7 @@ function normalizeAssignmentWorkspaceFolder(value) {
209
216
  return folderName;
210
217
  }
211
218
  function normalizeAssignmentAgent(value) {
212
- if (value !== 'CODEX' && value !== 'CLAUDE_CODE') {
219
+ if (value !== 'CODEX' && value !== 'CLAUDE_CODE' && value !== 'CURSOR_COMPOSER') {
213
220
  throw new Error('agent_task_assignment_agent_missing');
214
221
  }
215
222
  return value;
@@ -999,6 +1006,8 @@ function buildWorkerCapabilities(input) {
999
1006
  const codexAuthenticated = typeof input === 'string' ? true : input.codexAuthenticated === true;
1000
1007
  const claudeVersion = typeof input === 'string' ? null : input.claudeVersion?.trim() || null;
1001
1008
  const claudeAuthenticated = typeof input === 'string' ? false : input.claudeAuthenticated === true;
1009
+ const cursorVersion = typeof input === 'string' ? null : input.cursorVersion?.trim() || null;
1010
+ const cursorAuthenticated = typeof input === 'string' ? false : input.cursorAuthenticated === true;
1002
1011
  const npmVersion = typeof input === 'string' ? null : input.npmVersion?.trim() || null;
1003
1012
  const playwright = typeof input === 'string' ? null : input.playwright ?? null;
1004
1013
  const maxParallelTasks = typeof input === 'string'
@@ -1031,8 +1040,21 @@ function buildWorkerCapabilities(input) {
1031
1040
  ready: claudeAuthenticated,
1032
1041
  });
1033
1042
  }
1043
+ if (cursorVersion) {
1044
+ degradedReasons.push('cursor_runner_not_configured');
1045
+ if (!cursorAuthenticated) {
1046
+ degradedReasons.push('cursor_not_authenticated');
1047
+ }
1048
+ agents.push({
1049
+ agent: 'CURSOR_COMPOSER',
1050
+ cliVersion: cursorVersion,
1051
+ authenticated: cursorAuthenticated,
1052
+ models: [],
1053
+ ready: false,
1054
+ });
1055
+ }
1034
1056
  if (agents.length === 0) {
1035
- throw new Error('agent_cli_not_found: install and authenticate Codex or Claude Code before starting a PlayDrop worker.');
1057
+ throw new Error('agent_cli_not_found: install and authenticate Codex, Claude Code, or Cursor before starting a PlayDrop worker.');
1036
1058
  }
1037
1059
  return {
1038
1060
  agent: agents[0].agent,
@@ -1044,6 +1066,7 @@ function buildWorkerCapabilities(input) {
1044
1066
  agents,
1045
1067
  ready: agents.some((agent) => agent.ready === true),
1046
1068
  degradedReasons,
1069
+ wrapperContractVersion: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION', WORKER_WRAPPER_CONTRACT_VERSION),
1047
1070
  ...(npmVersion ? { npmVersion } : {}),
1048
1071
  ...(playwright ? { playwright } : {}),
1049
1072
  os: node_process_1.default.platform,
@@ -1655,10 +1678,762 @@ function probeClaudeInstallation() {
1655
1678
  }
1656
1679
  return { claudeVersion: match[1] };
1657
1680
  }
1681
+ function probeCursorInstallation() {
1682
+ if (!(0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_ENABLE_CURSOR', false)) {
1683
+ return null;
1684
+ }
1685
+ const command = node_process_1.default.env.PLAYDROP_WORKER_CURSOR_COMMAND?.trim() || 'cursor';
1686
+ const probe = (0, shellProbe_1.runShell)((0, shellProbe_1.buildCommandPathProbe)(command));
1687
+ if (probe.exitCode !== 0 || !probe.output.trim()) {
1688
+ throw new Error(`worker_preflight_cursor_missing: ${command} was not found. Disable PLAYDROP_WORKER_ENABLE_CURSOR or install the configured Cursor command.`);
1689
+ }
1690
+ const versionResult = (0, shellProbe_1.runShell)(`${command} --version`);
1691
+ const version = versionResult.output.trim().split(/\r?\n/)[0]?.trim() || '';
1692
+ if (!version || versionResult.exitCode !== 0) {
1693
+ throw new Error(`cursor_version_check_failed: "${command} --version" exited with code ${versionResult.exitCode} and reported no version. Output: ${versionResult.output.slice(0, 200)}`);
1694
+ }
1695
+ return { cursorVersion: version };
1696
+ }
1658
1697
  function readClaudeAuthenticated() {
1659
1698
  const status = (0, shellProbe_1.runShell)('claude auth status --text');
1660
1699
  return status.exitCode === 0;
1661
1700
  }
1701
+ function readCursorAuthenticated() {
1702
+ return (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_CURSOR_AUTHENTICATED', false);
1703
+ }
1704
+ function errorMessage(error) {
1705
+ return error instanceof Error ? error.message : String(error);
1706
+ }
1707
+ function errorCode(error) {
1708
+ return errorMessage(error).split(':')[0]?.trim() || 'unknown_error';
1709
+ }
1710
+ function readPlaydropPluginVersion(pluginRoot) {
1711
+ for (const relativePath of ['.codex-plugin/plugin.json', 'plugin.json', 'package.json']) {
1712
+ try {
1713
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(pluginRoot, relativePath), 'utf8'));
1714
+ const version = typeof parsed.version === 'string' ? parsed.version.trim() : '';
1715
+ if (version) {
1716
+ return version;
1717
+ }
1718
+ }
1719
+ catch {
1720
+ continue;
1721
+ }
1722
+ }
1723
+ return null;
1724
+ }
1725
+ function collectWorkerLocalStatus() {
1726
+ const degradedReasons = [];
1727
+ let codexVersion = null;
1728
+ let claudeVersion = null;
1729
+ let cursorVersion = null;
1730
+ let npmVersion = null;
1731
+ let playwright = null;
1732
+ let pluginRoot = null;
1733
+ let pluginVersion = null;
1734
+ let pluginError = null;
1735
+ try {
1736
+ codexVersion = probeCodexInstallation()?.codexVersion ?? null;
1737
+ }
1738
+ catch (error) {
1739
+ degradedReasons.push(errorCode(error));
1740
+ }
1741
+ try {
1742
+ claudeVersion = probeClaudeInstallation()?.claudeVersion ?? null;
1743
+ }
1744
+ catch (error) {
1745
+ degradedReasons.push(errorCode(error));
1746
+ }
1747
+ try {
1748
+ cursorVersion = probeCursorInstallation()?.cursorVersion ?? null;
1749
+ }
1750
+ catch (error) {
1751
+ degradedReasons.push(errorCode(error));
1752
+ }
1753
+ try {
1754
+ npmVersion = probeNpmVersion();
1755
+ }
1756
+ catch (error) {
1757
+ degradedReasons.push(errorCode(error));
1758
+ }
1759
+ try {
1760
+ playwright = probePlaywrightChromium();
1761
+ }
1762
+ catch (error) {
1763
+ degradedReasons.push(errorCode(error));
1764
+ }
1765
+ try {
1766
+ pluginRoot = resolvePlaydropPluginRoot();
1767
+ pluginVersion = readPlaydropPluginVersion(pluginRoot);
1768
+ }
1769
+ catch (error) {
1770
+ pluginError = errorMessage(error);
1771
+ degradedReasons.push(errorCode(error));
1772
+ }
1773
+ let capabilities = null;
1774
+ try {
1775
+ capabilities = buildWorkerCapabilities({
1776
+ codexVersion,
1777
+ codexAuthenticated: codexVersion ? readCodexAuthenticated() : false,
1778
+ claudeVersion,
1779
+ claudeAuthenticated: claudeVersion ? readClaudeAuthenticated() : false,
1780
+ cursorVersion,
1781
+ cursorAuthenticated: cursorVersion ? readCursorAuthenticated() : false,
1782
+ npmVersion,
1783
+ playwright,
1784
+ runningTaskCount: 0,
1785
+ maxParallelTasks: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS),
1786
+ });
1787
+ if (pluginVersion) {
1788
+ capabilities.pluginVersion = pluginVersion;
1789
+ for (const agent of capabilities.agents) {
1790
+ agent.pluginVersion = pluginVersion;
1791
+ }
1792
+ }
1793
+ for (const reason of capabilities.degradedReasons ?? []) {
1794
+ degradedReasons.push(reason);
1795
+ }
1796
+ }
1797
+ catch (error) {
1798
+ degradedReasons.push(errorCode(error));
1799
+ }
1800
+ const uniqueReasons = Array.from(new Set(degradedReasons));
1801
+ const pluginReady = Boolean(pluginRoot && !pluginError);
1802
+ const infrastructureReady = Boolean(npmVersion && playwright?.chromiumInstalled && pluginReady);
1803
+ const ready = Boolean(capabilities?.ready) && infrastructureReady;
1804
+ return {
1805
+ ready,
1806
+ degradedReasons: ready ? [] : uniqueReasons,
1807
+ cliVersion: (0, clientInfo_1.getCliVersion)(),
1808
+ nodeVersion: node_process_1.default.versions.node,
1809
+ npmVersion,
1810
+ playwright,
1811
+ plugin: {
1812
+ ready: pluginReady,
1813
+ root: pluginRoot,
1814
+ version: pluginVersion,
1815
+ error: pluginError,
1816
+ },
1817
+ agents: capabilities?.agents ?? [],
1818
+ capabilities,
1819
+ };
1820
+ }
1821
+ function getWorkerStateDir() {
1822
+ return node_path_1.default.join(node_os_1.default.homedir(), '.playdrop', 'worker');
1823
+ }
1824
+ function workerRuntimeStateFilePath(target) {
1825
+ const normalizedTarget = target === 'FIRST_PARTY' ? 'first-party' : 'personal';
1826
+ return node_path_1.default.join(getWorkerStateDir(), `${normalizedTarget}-runtime.json`);
1827
+ }
1828
+ function isProcessAlive(pid) {
1829
+ if (!Number.isInteger(pid) || pid <= 0) {
1830
+ return false;
1831
+ }
1832
+ try {
1833
+ node_process_1.default.kill(pid, 0);
1834
+ return true;
1835
+ }
1836
+ catch (error) {
1837
+ const code = error.code;
1838
+ return code === 'EPERM';
1839
+ }
1840
+ }
1841
+ function writeWorkerRuntimeState(state) {
1842
+ const file = workerRuntimeStateFilePath(state.target);
1843
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(file), { recursive: true });
1844
+ (0, node_fs_1.writeFileSync)(file, JSON.stringify({
1845
+ schemaVersion: 1,
1846
+ pid: node_process_1.default.pid,
1847
+ startedAt: new Date().toISOString(),
1848
+ ...state,
1849
+ }, null, 2));
1850
+ }
1851
+ function clearWorkerRuntimeState(target) {
1852
+ (0, node_fs_1.rmSync)(workerRuntimeStateFilePath(target), { force: true });
1853
+ }
1854
+ function readActivePersonalWorkerRuntimeState() {
1855
+ const file = workerRuntimeStateFilePath('PERSONAL');
1856
+ let parsed;
1857
+ try {
1858
+ parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
1859
+ }
1860
+ catch {
1861
+ return null;
1862
+ }
1863
+ if (!parsed || parsed.target !== 'PERSONAL' || !isProcessAlive(parsed.pid)) {
1864
+ return null;
1865
+ }
1866
+ return parsed;
1867
+ }
1868
+ function getManagedWorkerPolicyFilePath(env, workerKey) {
1869
+ return node_path_1.default.join(getWorkerStateDir(), `update-policy-${env}-${workerKey}.json`);
1870
+ }
1871
+ function writeManagedWorkerUpdatePolicyFile(input) {
1872
+ if (!input.updatePolicy) {
1873
+ return;
1874
+ }
1875
+ const policyFile = getManagedWorkerPolicyFilePath(input.env, input.workerKey);
1876
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(policyFile), { recursive: true });
1877
+ (0, node_fs_1.writeFileSync)(policyFile, JSON.stringify({
1878
+ schemaVersion: 1,
1879
+ writtenAt: new Date().toISOString(),
1880
+ targetCliVersion: input.updatePolicy.targetCliVersion,
1881
+ minimumCliVersion: input.updatePolicy.minimumCliVersion,
1882
+ targetPluginVersion: input.updatePolicy.targetPluginVersion,
1883
+ minimumPluginVersion: input.updatePolicy.minimumPluginVersion,
1884
+ managedAgentCliVersions: input.updatePolicy.managedAgentCliVersions ?? {},
1885
+ requiredWrapperContractVersion: input.updatePolicy.requiredWrapperContractVersion,
1886
+ }, null, 2));
1887
+ }
1888
+ function shellQuote(value) {
1889
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1890
+ }
1891
+ function validatePackageVersion(value, label) {
1892
+ const version = typeof value === 'string' ? value.trim() : '';
1893
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
1894
+ throw new Error(`${label}_invalid:${version || 'missing'}`);
1895
+ }
1896
+ return version;
1897
+ }
1898
+ function verifyNpmGlobalInstallPreconditions() {
1899
+ const prefixProbe = (0, shellProbe_1.runShell)('npm prefix -g');
1900
+ const prefix = prefixProbe.output.trim().split(/\r?\n/)[0]?.trim() ?? '';
1901
+ if (prefixProbe.exitCode !== 0 || !prefix) {
1902
+ throw new Error(`worker_update_npm_prefix_unavailable: npm prefix -g exited with code ${prefixProbe.exitCode}.`);
1903
+ }
1904
+ const writeProbe = (0, shellProbe_1.runShell)(`[ -w ${shellQuote(prefix)} ]`);
1905
+ if (writeProbe.exitCode !== 0) {
1906
+ throw new Error(`worker_update_npm_prefix_not_writable: ${prefix} is not writable by ${node_os_1.default.userInfo().username}. Install Node with a user-writable global prefix before enabling managed worker updates.`);
1907
+ }
1908
+ return { prefix };
1909
+ }
1910
+ function managedWorkerWrapperPath(env) {
1911
+ return node_path_1.default.join(getWorkerStateDir(), `playdrop-worker-${env}.sh`);
1912
+ }
1913
+ function managedWorkerLaunchAgentLabel(env) {
1914
+ return `ai.playdrop.worker.${env}`;
1915
+ }
1916
+ function managedWorkerLaunchAgentPath(env) {
1917
+ return node_path_1.default.join(node_os_1.default.homedir(), 'Library', 'LaunchAgents', `${managedWorkerLaunchAgentLabel(env)}.plist`);
1918
+ }
1919
+ function escapeXml(value) {
1920
+ return value
1921
+ .replace(/&/g, '&amp;')
1922
+ .replace(/</g, '&lt;')
1923
+ .replace(/>/g, '&gt;')
1924
+ .replace(/"/g, '&quot;')
1925
+ .replace(/'/g, '&apos;');
1926
+ }
1927
+ function unescapeXml(value) {
1928
+ return value
1929
+ .replace(/&apos;/g, "'")
1930
+ .replace(/&quot;/g, '"')
1931
+ .replace(/&gt;/g, '>')
1932
+ .replace(/&lt;/g, '<')
1933
+ .replace(/&amp;/g, '&');
1934
+ }
1935
+ function parseLaunchAgentProgramArguments(plist) {
1936
+ const arrayMatch = /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/i.exec(plist);
1937
+ if (!arrayMatch?.[1]) {
1938
+ return [];
1939
+ }
1940
+ return Array.from(arrayMatch[1].matchAll(/<string>([\s\S]*?)<\/string>/gi))
1941
+ .map((match) => unescapeXml(match[1].trim()))
1942
+ .filter(Boolean);
1943
+ }
1944
+ function readLaunchAgentArguments(plistPath) {
1945
+ return parseLaunchAgentProgramArguments((0, node_fs_1.readFileSync)(plistPath, 'utf8'));
1946
+ }
1947
+ function readArgumentValue(args, flag) {
1948
+ for (let index = 0; index < args.length; index += 1) {
1949
+ const arg = args[index];
1950
+ if (arg === flag) {
1951
+ return args[index + 1] ?? null;
1952
+ }
1953
+ if (arg.startsWith(`${flag}=`)) {
1954
+ return arg.slice(flag.length + 1);
1955
+ }
1956
+ }
1957
+ return null;
1958
+ }
1959
+ function isPlaydropWorkerStartCommand(args) {
1960
+ const joined = args.join(' ');
1961
+ return /\bplaydrop\b/.test(joined) && args.includes('worker') && args.includes('start');
1962
+ }
1963
+ function isCompatibleLegacyWorkerLaunchAgent(input) {
1964
+ if (!isPlaydropWorkerStartCommand(input.args)) {
1965
+ return false;
1966
+ }
1967
+ return readArgumentValue(input.args, '--env') === input.env
1968
+ && readArgumentValue(input.args, '--name') === input.workerName;
1969
+ }
1970
+ function assertLaunchAgentArgumentsCompatible(input) {
1971
+ const normalizedPlistPath = node_path_1.default.resolve(input.plistPath);
1972
+ const normalizedCanonicalPath = node_path_1.default.resolve(input.canonicalPlistPath);
1973
+ if (normalizedPlistPath === normalizedCanonicalPath) {
1974
+ if (input.args.length === 1 && input.args[0] === input.wrapperPath) {
1975
+ return;
1976
+ }
1977
+ throw new Error(`worker_supervisor_conflict: ${input.plistPath} already exists but does not point at the managed PlayDrop worker wrapper ${input.wrapperPath}. Refusing to overwrite it.`);
1978
+ }
1979
+ if (!isPlaydropWorkerStartCommand(input.args) && input.args[0] !== input.wrapperPath) {
1980
+ return;
1981
+ }
1982
+ if (input.args[0] === input.wrapperPath
1983
+ || isCompatibleLegacyWorkerLaunchAgent({
1984
+ args: input.args,
1985
+ env: input.env,
1986
+ workerName: input.workerName,
1987
+ })) {
1988
+ return;
1989
+ }
1990
+ throw new Error(`worker_supervisor_conflict: ${input.plistPath} appears to manage a different PlayDrop worker. Refusing to install a second supervisor.`);
1991
+ }
1992
+ function assertLaunchAgentPlistCompatible(input) {
1993
+ assertLaunchAgentArgumentsCompatible({
1994
+ plistPath: input.plistPath,
1995
+ args: parseLaunchAgentProgramArguments(input.plist),
1996
+ env: input.env,
1997
+ workerName: input.workerName,
1998
+ wrapperPath: input.wrapperPath,
1999
+ canonicalPlistPath: input.canonicalPlistPath,
2000
+ });
2001
+ }
2002
+ function assertNoSupervisorConflicts(input) {
2003
+ const launchAgentsDir = node_path_1.default.dirname(input.canonicalPlistPath);
2004
+ if ((0, node_fs_1.existsSync)(input.canonicalPlistPath)) {
2005
+ assertLaunchAgentArgumentsCompatible({
2006
+ ...input,
2007
+ plistPath: input.canonicalPlistPath,
2008
+ args: readLaunchAgentArguments(input.canonicalPlistPath),
2009
+ });
2010
+ }
2011
+ if (!(0, node_fs_1.existsSync)(launchAgentsDir)) {
2012
+ return;
2013
+ }
2014
+ for (const entry of (0, node_fs_1.readdirSync)(launchAgentsDir)) {
2015
+ if (!entry.endsWith('.plist')) {
2016
+ continue;
2017
+ }
2018
+ const plistPath = node_path_1.default.join(launchAgentsDir, entry);
2019
+ if (node_path_1.default.resolve(plistPath) === node_path_1.default.resolve(input.canonicalPlistPath)) {
2020
+ continue;
2021
+ }
2022
+ let args;
2023
+ try {
2024
+ args = readLaunchAgentArguments(plistPath);
2025
+ }
2026
+ catch {
2027
+ continue;
2028
+ }
2029
+ assertLaunchAgentArgumentsCompatible({
2030
+ ...input,
2031
+ plistPath,
2032
+ args,
2033
+ });
2034
+ }
2035
+ }
2036
+ async function writeManagedWorkerWrapper(input) {
2037
+ const wrapperPath = managedWorkerWrapperPath(input.env);
2038
+ const policyFile = getManagedWorkerPolicyFilePath(input.env, input.workerKey);
2039
+ const failureFile = node_path_1.default.join(getWorkerStateDir(), `update-failure-${input.env}-${input.workerKey}.json`);
2040
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(wrapperPath), { recursive: true });
2041
+ const script = `#!/bin/sh
2042
+ set -eu
2043
+
2044
+ export PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION="${WORKER_WRAPPER_CONTRACT_VERSION}"
2045
+ POLICY_FILE="${policyFile}"
2046
+ FAILURE_FILE="${failureFile}"
2047
+ BACKOFF_SECONDS="\${PLAYDROP_WORKER_UPDATE_FAILURE_BACKOFF_SECONDS:-30}"
2048
+
2049
+ record_failure() {
2050
+ node - "$FAILURE_FILE" "$1" <<'NODE'
2051
+ const fs = require("fs");
2052
+ const path = require("path");
2053
+ const file = process.argv[2];
2054
+ const error = process.argv[3] || "worker_update_install_failed";
2055
+ fs.mkdirSync(path.dirname(file), { recursive: true });
2056
+ fs.writeFileSync(file, JSON.stringify({
2057
+ schemaVersion: 1,
2058
+ writtenAt: new Date().toISOString(),
2059
+ error,
2060
+ }, null, 2));
2061
+ NODE
2062
+ }
2063
+
2064
+ fail_converge() {
2065
+ code="$1"
2066
+ reason="$2"
2067
+ record_failure "$reason"
2068
+ sleep "$BACKOFF_SECONDS"
2069
+ exit "$code"
2070
+ }
2071
+
2072
+ run_or_fail() {
2073
+ reason="$1"
2074
+ shift
2075
+ "$@" || fail_converge "$?" "$reason"
2076
+ }
2077
+
2078
+ validate_policy_file() {
2079
+ if [ ! -f "$POLICY_FILE" ]; then
2080
+ return 0
2081
+ fi
2082
+ node - "$POLICY_FILE" <<'NODE' || fail_converge "$?" "worker_update_policy_unavailable"
2083
+ const fs = require("fs");
2084
+ const file = process.argv[2];
2085
+ try {
2086
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
2087
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2088
+ process.exit(1);
2089
+ }
2090
+ } catch {
2091
+ process.exit(1);
2092
+ }
2093
+ NODE
2094
+ }
2095
+
2096
+ read_policy_value() {
2097
+ node -e 'const fs = require("fs"); const file = process.argv[1]; const key = process.argv[2]; if (!fs.existsSync(file)) process.exit(0); const policy = JSON.parse(fs.readFileSync(file, "utf8")); const value = policy && policy[key]; if (typeof value === "string") process.stdout.write(value);' "$POLICY_FILE" "$1"
2098
+ }
2099
+
2100
+ validate_policy_file
2101
+ TARGET_CLI_VERSION="$(read_policy_value targetCliVersion)"
2102
+ TARGET_PLUGIN_VERSION="$(read_policy_value targetPluginVersion)"
2103
+
2104
+ if [ -n "$TARGET_CLI_VERSION" ]; then
2105
+ run_or_fail "worker_update_cli_install_failed" npm install -g --no-audit --no-fund "@playdrop/playdrop-cli@$TARGET_CLI_VERSION"
2106
+ fi
2107
+
2108
+ if [ -n "$TARGET_PLUGIN_VERSION" ]; then
2109
+ if command -v codex >/dev/null 2>&1; then
2110
+ run_or_fail "worker_update_plugin_install_failed:codex" playdrop agents update-plugin codex
2111
+ fi
2112
+ if command -v claude >/dev/null 2>&1; then
2113
+ run_or_fail "worker_update_plugin_install_failed:claude" playdrop agents update-plugin claude
2114
+ fi
2115
+ node - "$TARGET_PLUGIN_VERSION" <<'NODE' || fail_converge "$?" "worker_update_plugin_version_mismatch"
2116
+ const childProcess = require("child_process");
2117
+ const targetVersion = process.argv[2];
2118
+ const result = childProcess.spawnSync("playdrop", ["agents", "status", "--json"], { encoding: "utf8" });
2119
+ if (result.status !== 0) {
2120
+ process.exit(1);
2121
+ }
2122
+ let parsed;
2123
+ try {
2124
+ parsed = JSON.parse(result.stdout);
2125
+ } catch {
2126
+ process.exit(1);
2127
+ }
2128
+ const agents = Array.isArray(parsed && parsed.agents) ? parsed.agents : [];
2129
+ const checked = agents.filter((agent) => agent && agent.id !== "cursor" && agent.cli && agent.cli.installed);
2130
+ for (const agent of checked) {
2131
+ const version = agent && agent.plugin && typeof agent.plugin.version === "string" ? agent.plugin.version.trim() : "";
2132
+ if (version !== targetVersion) {
2133
+ process.exit(1);
2134
+ }
2135
+ }
2136
+ NODE
2137
+ fi
2138
+
2139
+ node - "$POLICY_FILE" <<'NODE' || fail_converge "$?" "worker_update_agent_cli_install_failed"
2140
+ const childProcess = require("child_process");
2141
+ const fs = require("fs");
2142
+
2143
+ const policyFile = process.argv[2];
2144
+ if (!policyFile || !fs.existsSync(policyFile)) {
2145
+ process.exit(0);
2146
+ }
2147
+ const policy = JSON.parse(fs.readFileSync(policyFile, "utf8"));
2148
+ const versions = policy && policy.managedAgentCliVersions && typeof policy.managedAgentCliVersions === "object"
2149
+ ? policy.managedAgentCliVersions
2150
+ : {};
2151
+ const packages = {
2152
+ CODEX: "@openai/codex",
2153
+ CLAUDE_CODE: "@anthropic-ai/claude-code",
2154
+ };
2155
+ const aliases = {
2156
+ CODEX: "CODEX",
2157
+ OPENAI_CODEX: "CODEX",
2158
+ CLAUDE: "CLAUDE_CODE",
2159
+ CLAUDE_CODE: "CLAUDE_CODE",
2160
+ CURSOR: "CURSOR_COMPOSER",
2161
+ CURSOR_COMPOSER: "CURSOR_COMPOSER",
2162
+ };
2163
+ for (const [rawKey, rawVersion] of Object.entries(versions)) {
2164
+ const version = typeof rawVersion === "string" ? rawVersion.trim() : "";
2165
+ if (!/^\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
2166
+ throw new Error(\`worker_update_agent_cli_version_invalid:\${rawKey}\`);
2167
+ }
2168
+ const normalizedKey = aliases[String(rawKey).trim().toUpperCase().replace(/[-\\s]+/g, "_")];
2169
+ if (normalizedKey === "CURSOR_COMPOSER") {
2170
+ throw new Error("worker_update_cursor_cli_auto_update_unsupported");
2171
+ }
2172
+ const packageName = normalizedKey ? packages[normalizedKey] : null;
2173
+ if (!packageName) {
2174
+ throw new Error(\`worker_update_agent_cli_unknown:\${rawKey}\`);
2175
+ }
2176
+ const result = childProcess.spawnSync(
2177
+ "npm",
2178
+ ["install", "-g", "--no-audit", "--no-fund", \`\${packageName}@\${version}\`],
2179
+ { stdio: "inherit" },
2180
+ );
2181
+ if (result.status !== 0) {
2182
+ throw new Error(\`worker_update_agent_cli_install_failed:\${normalizedKey}\`);
2183
+ }
2184
+ }
2185
+ NODE
2186
+
2187
+ exec playdrop worker start --env ${shellQuote(input.env)} --name ${shellQuote(input.workerName)}
2188
+ `;
2189
+ await (0, promises_1.writeFile)(wrapperPath, script, 'utf8');
2190
+ await (0, promises_1.chmod)(wrapperPath, 0o755);
2191
+ return { wrapperPath, policyFile };
2192
+ }
2193
+ async function writeManagedWorkerLaunchAgent(input) {
2194
+ if (node_process_1.default.platform !== 'darwin') {
2195
+ throw new Error('worker_setup_launchagent_unsupported: managed worker setup currently supports macOS LaunchAgent only.');
2196
+ }
2197
+ const label = managedWorkerLaunchAgentLabel(input.env);
2198
+ const plistPath = managedWorkerLaunchAgentPath(input.env);
2199
+ const logDir = node_path_1.default.join(getWorkerStateDir(), 'logs');
2200
+ assertNoSupervisorConflicts({
2201
+ env: input.env,
2202
+ workerName: input.workerName,
2203
+ wrapperPath: input.wrapperPath,
2204
+ canonicalPlistPath: plistPath,
2205
+ });
2206
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(plistPath), { recursive: true });
2207
+ await (0, promises_1.mkdir)(logDir, { recursive: true });
2208
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
2209
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2210
+ <plist version="1.0">
2211
+ <dict>
2212
+ <key>Label</key>
2213
+ <string>${escapeXml(label)}</string>
2214
+ <key>ProgramArguments</key>
2215
+ <array>
2216
+ <string>${escapeXml(input.wrapperPath)}</string>
2217
+ </array>
2218
+ <key>RunAtLoad</key>
2219
+ <true/>
2220
+ <key>KeepAlive</key>
2221
+ <true/>
2222
+ <key>EnvironmentVariables</key>
2223
+ <dict>
2224
+ <key>PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION</key>
2225
+ <string>${WORKER_WRAPPER_CONTRACT_VERSION}</string>
2226
+ </dict>
2227
+ <key>StandardOutPath</key>
2228
+ <string>${escapeXml(node_path_1.default.join(logDir, `${label}.out.log`))}</string>
2229
+ <key>StandardErrorPath</key>
2230
+ <string>${escapeXml(node_path_1.default.join(logDir, `${label}.err.log`))}</string>
2231
+ </dict>
2232
+ </plist>
2233
+ `;
2234
+ await (0, promises_1.writeFile)(plistPath, plist, 'utf8');
2235
+ return { label, plistPath };
2236
+ }
2237
+ function launchManagedWorker(label, plistPath) {
2238
+ const uid = typeof node_process_1.default.getuid === 'function' ? node_process_1.default.getuid() : null;
2239
+ if (uid === null) {
2240
+ throw new Error('worker_setup_launchagent_uid_unavailable');
2241
+ }
2242
+ const domain = `gui/${uid}`;
2243
+ const bootstrap = (0, shellProbe_1.runShell)(`launchctl bootstrap ${shellQuote(domain)} ${shellQuote(plistPath)}`);
2244
+ if (bootstrap.exitCode !== 0 && !/already|exists|service name is already registered/i.test(bootstrap.output)) {
2245
+ throw new Error(`worker_setup_launchagent_start_failed: launchctl bootstrap exited with code ${bootstrap.exitCode}. ${bootstrap.output.slice(0, 300)}`);
2246
+ }
2247
+ const kickstart = (0, shellProbe_1.runShell)(`launchctl kickstart -k ${shellQuote(`${domain}/${label}`)}`);
2248
+ if (kickstart.exitCode !== 0) {
2249
+ throw new Error(`worker_setup_launchagent_start_failed: launchctl kickstart exited with code ${kickstart.exitCode}. ${kickstart.output.slice(0, 300)}`);
2250
+ }
2251
+ }
2252
+ async function runPluginSetupAction(action, agent) {
2253
+ const previousExitCode = node_process_1.default.exitCode;
2254
+ node_process_1.default.exitCode = undefined;
2255
+ try {
2256
+ if (action === 'install') {
2257
+ await (0, agents_1.installPlugin)(agent);
2258
+ }
2259
+ else {
2260
+ await (0, agents_1.updatePlugin)(agent);
2261
+ }
2262
+ if (node_process_1.default.exitCode && node_process_1.default.exitCode !== 0) {
2263
+ throw new Error(`worker_setup_plugin_${action}_failed:${agent}`);
2264
+ }
2265
+ }
2266
+ finally {
2267
+ node_process_1.default.exitCode = previousExitCode;
2268
+ }
2269
+ }
2270
+ async function ensurePlaydropPluginsForSetup(local) {
2271
+ for (const agent of local.agents) {
2272
+ if (agent.agent === 'CODEX') {
2273
+ await runPluginSetupAction(local.plugin.ready ? 'update' : 'install', 'codex');
2274
+ }
2275
+ else if (agent.agent === 'CLAUDE_CODE') {
2276
+ await runPluginSetupAction(local.plugin.ready ? 'update' : 'install', 'claude');
2277
+ }
2278
+ }
2279
+ }
2280
+ async function resolveWorkerStatusPayload(options = {}) {
2281
+ loadWorkerEnvFile(options.env);
2282
+ const local = collectWorkerLocalStatus();
2283
+ const server = {
2284
+ reachable: false,
2285
+ authenticated: false,
2286
+ worker: null,
2287
+ };
2288
+ let ctx;
2289
+ try {
2290
+ ctx = await (0, commandContext_1.resolveOptionalEnvironmentContext)('worker status', { env: options.env });
2291
+ }
2292
+ catch (error) {
2293
+ return {
2294
+ schemaVersion: 1,
2295
+ generatedAt: new Date().toISOString(),
2296
+ local,
2297
+ server: {
2298
+ ...server,
2299
+ error: errorCode(error),
2300
+ },
2301
+ };
2302
+ }
2303
+ if (!ctx?.token) {
2304
+ return {
2305
+ schemaVersion: 1,
2306
+ generatedAt: new Date().toISOString(),
2307
+ local,
2308
+ server: {
2309
+ ...server,
2310
+ error: 'not_authenticated',
2311
+ },
2312
+ };
2313
+ }
2314
+ server.authenticated = true;
2315
+ const workerKey = (0, config_1.loadConfig)().workerKey?.trim() ?? '';
2316
+ try {
2317
+ const workers = await ctx.client.listMyAgentWorkers();
2318
+ server.reachable = true;
2319
+ server.worker = workerKey
2320
+ ? workers.workers.find((worker) => worker.workerKey === workerKey) ?? null
2321
+ : null;
2322
+ }
2323
+ catch (error) {
2324
+ server.error = errorCode(error);
2325
+ }
2326
+ return {
2327
+ schemaVersion: 1,
2328
+ generatedAt: new Date().toISOString(),
2329
+ local,
2330
+ server,
2331
+ };
2332
+ }
2333
+ function printWorkerStatus(payload) {
2334
+ console.log(`Local worker: ${payload.local.ready ? 'ready' : 'degraded'}`);
2335
+ console.log(` CLI: ${payload.local.cliVersion}`);
2336
+ console.log(` Node: ${payload.local.nodeVersion}`);
2337
+ console.log(` npm: ${payload.local.npmVersion ?? 'missing'}`);
2338
+ console.log(` PlayDrop plugin: ${payload.local.plugin.ready ? payload.local.plugin.version ?? payload.local.plugin.root ?? 'installed' : 'missing'}`);
2339
+ if (payload.local.agents.length > 0) {
2340
+ for (const agent of payload.local.agents) {
2341
+ console.log(` ${agent.agent}: ${agent.ready ? 'ready' : 'degraded'} ${agent.cliVersion ?? ''}`.trimEnd());
2342
+ }
2343
+ }
2344
+ else {
2345
+ console.log(' Agents: none detected');
2346
+ }
2347
+ if (payload.local.degradedReasons.length > 0) {
2348
+ console.log(` Reasons: ${payload.local.degradedReasons.join(', ')}`);
2349
+ }
2350
+ if (!payload.server.authenticated) {
2351
+ console.log('Server worker: not authenticated');
2352
+ }
2353
+ else if (!payload.server.reachable) {
2354
+ console.log(`Server worker: unreachable${payload.server.error ? ` (${payload.server.error})` : ''}`);
2355
+ }
2356
+ else if (!payload.server.worker) {
2357
+ console.log('Server worker: none registered');
2358
+ }
2359
+ else {
2360
+ console.log(`Server worker: ${payload.server.worker.name} (${payload.server.worker.lifecycleState ?? 'unknown'}, ${payload.server.worker.updateState ?? 'unknown'})`);
2361
+ }
2362
+ }
2363
+ async function showWorkerStatus(options = {}) {
2364
+ const payload = await resolveWorkerStatusPayload(options);
2365
+ if (options.json) {
2366
+ (0, output_1.printJson)(payload);
2367
+ }
2368
+ else {
2369
+ printWorkerStatus(payload);
2370
+ }
2371
+ return payload;
2372
+ }
2373
+ async function setupWorker(options = {}) {
2374
+ loadWorkerEnvFile(options.env);
2375
+ await (0, commandContext_1.withEnvironment)('worker setup', 'Setting up the PlayDrop worker', async ({ client, env }) => {
2376
+ const me = await client.me();
2377
+ const username = me.user?.username?.trim();
2378
+ if (!username) {
2379
+ throw new Error('worker_session_invalid: the stored session did not resolve to a user. Run "playdrop auth login" and retry.');
2380
+ }
2381
+ const target = resolveWorkerExecutionTargetFromRole(me.user.role);
2382
+ const workerKey = (0, config_1.getOrCreateWorkerKey)();
2383
+ const explicitWorkerName = options.name?.trim() || '';
2384
+ const workerName = explicitWorkerName || `${username} - ${node_os_1.default.hostname()}`;
2385
+ const local = collectWorkerLocalStatus();
2386
+ const agentReady = local.agents.some((agent) => agent.ready === true);
2387
+ const baseRuntimeReady = Boolean(agentReady && local.npmVersion && local.playwright?.chromiumInstalled);
2388
+ if (!baseRuntimeReady) {
2389
+ throw new Error(`worker_setup_preflight_failed: ${local.degradedReasons.join(', ') || 'local worker runtime is not ready'}`);
2390
+ }
2391
+ await ensurePlaydropPluginsForSetup(local);
2392
+ const postPluginLocal = collectWorkerLocalStatus();
2393
+ if (!postPluginLocal.plugin.ready) {
2394
+ throw new Error(`worker_setup_plugin_unavailable: ${postPluginLocal.plugin.error ?? 'PlayDrop plugin could not be verified after setup'}`);
2395
+ }
2396
+ if (target !== 'FIRST_PARTY') {
2397
+ (0, output_1.printSuccess)('Personal worker setup complete.', [
2398
+ 'Run: playdrop worker start --supervise-stdin',
2399
+ ]);
2400
+ return;
2401
+ }
2402
+ if (!options.env?.trim()) {
2403
+ throw new Error('worker_setup_requires_env: admin worker setup must pass --env <env> so the LaunchAgent cannot be installed for the wrong environment.');
2404
+ }
2405
+ if (!explicitWorkerName) {
2406
+ throw new Error('worker_setup_requires_name: admin worker setup must pass --name <worker name> so the LaunchAgent identity is deterministic.');
2407
+ }
2408
+ verifyNpmGlobalInstallPreconditions();
2409
+ const { wrapperPath, policyFile } = await writeManagedWorkerWrapper({ env, workerName, workerKey });
2410
+ const { label, plistPath } = await writeManagedWorkerLaunchAgent({ env, workerName, wrapperPath });
2411
+ const status = await resolveWorkerStatusPayload({ env });
2412
+ const activeTaskCount = status.server.worker?.activeTaskCount ?? 0;
2413
+ const shouldStart = options.start !== false;
2414
+ if (activeTaskCount > 0 && shouldStart) {
2415
+ await client.workerCreateUpdateIntent({
2416
+ workerKey,
2417
+ environment: env,
2418
+ reason: 'setup_apply',
2419
+ });
2420
+ (0, output_1.printSuccess)('Managed worker setup updated and queued for quiesced restart.', [
2421
+ `Wrapper: ${wrapperPath}`,
2422
+ `LaunchAgent: ${plistPath}`,
2423
+ `Policy file: ${policyFile}`,
2424
+ ]);
2425
+ return;
2426
+ }
2427
+ if (shouldStart || options.restart) {
2428
+ launchManagedWorker(label, plistPath);
2429
+ }
2430
+ (0, output_1.printSuccess)('Managed worker setup complete.', [
2431
+ `Wrapper: ${wrapperPath}`,
2432
+ `LaunchAgent: ${plistPath}`,
2433
+ `Policy file: ${policyFile}`,
2434
+ ]);
2435
+ }, { env: options.env });
2436
+ }
1662
2437
  async function failTaskWithRetry(client, taskId, body) {
1663
2438
  try {
1664
2439
  await client.workerFailAgentTask(taskId, body);
@@ -1708,11 +2483,14 @@ async function startWorker(options = {}) {
1708
2483
  const target = resolveWorkerExecutionTargetFromRole(me.user.role);
1709
2484
  const codexVersion = probeCodexInstallation()?.codexVersion ?? null;
1710
2485
  const claudeVersion = probeClaudeInstallation()?.claudeVersion ?? null;
2486
+ const cursorVersion = probeCursorInstallation()?.cursorVersion ?? null;
1711
2487
  const codexAuthenticated = codexVersion ? readCodexAuthenticated() : false;
1712
2488
  const claudeAuthenticated = claudeVersion ? readClaudeAuthenticated() : false;
2489
+ const cursorAuthenticated = cursorVersion ? readCursorAuthenticated() : false;
1713
2490
  const npmVersion = probeNpmVersion();
1714
2491
  const playwright = probePlaywrightChromium();
1715
2492
  const playdropPluginRoot = resolvePlaydropPluginRoot();
2493
+ const playdropPluginVersion = readPlaydropPluginVersion(playdropPluginRoot);
1716
2494
  const workerKey = (0, config_1.getOrCreateWorkerKey)();
1717
2495
  const workerName = options.name?.trim() || `${username} - ${node_os_1.default.hostname()}`;
1718
2496
  const maxParallelTasks = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS);
@@ -1721,17 +2499,31 @@ async function startWorker(options = {}) {
1721
2499
  codexAuthenticated,
1722
2500
  claudeVersion,
1723
2501
  claudeAuthenticated,
2502
+ cursorVersion,
2503
+ cursorAuthenticated,
1724
2504
  npmVersion,
1725
2505
  playwright,
1726
2506
  maxParallelTasks,
1727
2507
  runningTaskCount: 0,
1728
2508
  });
2509
+ if (playdropPluginVersion) {
2510
+ capabilities.pluginVersion = playdropPluginVersion;
2511
+ for (const agent of capabilities.agents) {
2512
+ agent.pluginVersion = playdropPluginVersion;
2513
+ }
2514
+ }
1729
2515
  if (!capabilities.ready) {
1730
2516
  const reasons = Array.isArray(capabilities.degradedReasons) && capabilities.degradedReasons.length > 0
1731
2517
  ? capabilities.degradedReasons.join(', ')
1732
2518
  : 'no ready agent';
1733
2519
  throw new Error(`worker_preflight_failed: ${reasons}`);
1734
2520
  }
2521
+ writeWorkerRuntimeState({
2522
+ env,
2523
+ workerKey,
2524
+ workerName,
2525
+ target,
2526
+ });
1735
2527
  const activeTaskIds = new Set();
1736
2528
  const activeTaskRuns = new Map();
1737
2529
  const activeTerminators = new Map();
@@ -1739,6 +2531,7 @@ async function startWorker(options = {}) {
1739
2531
  const workerDevPortBase = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_DEV_PORT_BASE', DEFAULT_WORKER_DEV_PORT_BASE);
1740
2532
  let sessionExpired = false;
1741
2533
  let shuttingDown = false;
2534
+ let quiescingForUpdate = false;
1742
2535
  let shutdownSignal = null;
1743
2536
  let crashed = false;
1744
2537
  let fatalTaskError = null;
@@ -1758,6 +2551,12 @@ async function startWorker(options = {}) {
1758
2551
  name: workerName,
1759
2552
  environment: env,
1760
2553
  capabilities: currentCapabilities(),
2554
+ lifecycleState: quiescingForUpdate ? 'QUIESCING' : 'READY',
2555
+ updateState: quiescingForUpdate ? 'UPDATE_REQUIRED' : 'CURRENT',
2556
+ ready: capabilities.ready,
2557
+ activeTaskCount: activeTaskRuns.size,
2558
+ cliVersion: (0, clientInfo_1.getCliVersion)(),
2559
+ pluginVersion: capabilities.pluginVersion,
1761
2560
  });
1762
2561
  const recordFatalTaskError = (error) => {
1763
2562
  if (!fatalTaskError) {
@@ -1766,6 +2565,24 @@ async function startWorker(options = {}) {
1766
2565
  shuttingDown = true;
1767
2566
  terminateActiveTasks();
1768
2567
  };
2568
+ const handleWorkerAction = (response) => {
2569
+ const requiredWrapperVersion = response?.updatePolicy?.requiredWrapperContractVersion;
2570
+ if (typeof requiredWrapperVersion === 'number'
2571
+ && requiredWrapperVersion > WORKER_WRAPPER_CONTRACT_VERSION) {
2572
+ recordFatalTaskError(new Error(`worker_wrapper_update_required: run "playdrop worker setup --env ${env}" to install wrapper contract ${requiredWrapperVersion}.`));
2573
+ return;
2574
+ }
2575
+ if (response?.action !== 'quiesce_for_update' || quiescingForUpdate) {
2576
+ return;
2577
+ }
2578
+ writeManagedWorkerUpdatePolicyFile({
2579
+ env,
2580
+ workerKey,
2581
+ updatePolicy: response.updatePolicy,
2582
+ });
2583
+ quiescingForUpdate = true;
2584
+ console.log('Worker entering QUIESCING for managed update. No new tasks will be claimed.');
2585
+ };
1769
2586
  const signalHandler = (signal) => {
1770
2587
  shutdownSignal = signal;
1771
2588
  if (shuttingDown) {
@@ -1775,8 +2592,21 @@ async function startWorker(options = {}) {
1775
2592
  // Task runs finish their own lifecycle (flush, fail, clean) before exit.
1776
2593
  terminateActiveTasks();
1777
2594
  };
2595
+ const stdinShutdownHandler = () => {
2596
+ shutdownSignal = 'SIGTERM';
2597
+ if (shuttingDown) {
2598
+ return;
2599
+ }
2600
+ shuttingDown = true;
2601
+ terminateActiveTasks();
2602
+ };
1778
2603
  node_process_1.default.once('SIGINT', signalHandler);
1779
2604
  node_process_1.default.once('SIGTERM', signalHandler);
2605
+ if (options.superviseStdin) {
2606
+ node_process_1.default.stdin.once('end', stdinShutdownHandler);
2607
+ node_process_1.default.stdin.once('close', stdinShutdownHandler);
2608
+ node_process_1.default.stdin.resume();
2609
+ }
1780
2610
  console.log(`PlayDrop Worker started as "${workerName}" (${target}).`);
1781
2611
  await sendWorkerHealthAlertSafely({ state: 'started', env, workerName });
1782
2612
  // Marks the session expired, kills any in-flight agent child, and lets the
@@ -1790,7 +2620,7 @@ async function startWorker(options = {}) {
1790
2620
  terminateActiveTasks();
1791
2621
  };
1792
2622
  try {
1793
- await client.workerPresence(presenceBody());
2623
+ handleWorkerAction(await client.workerPresence(presenceBody()));
1794
2624
  }
1795
2625
  catch (error) {
1796
2626
  if (isWorkerAuthFailureError(error)) {
@@ -1799,7 +2629,7 @@ async function startWorker(options = {}) {
1799
2629
  throw error;
1800
2630
  }
1801
2631
  presenceTimer = setInterval(() => {
1802
- client.workerPresence(presenceBody()).catch((error) => {
2632
+ client.workerPresence(presenceBody()).then(handleWorkerAction).catch((error) => {
1803
2633
  if (isWorkerAuthFailureError(error)) {
1804
2634
  handleSessionExpiry();
1805
2635
  return;
@@ -2009,8 +2839,8 @@ async function startWorker(options = {}) {
2009
2839
  throw new Error('worker_shutdown');
2010
2840
  }
2011
2841
  agentStartedAt = new Date();
2012
- agentResult = assignment.agent === 'CODEX'
2013
- ? await runCodex({
2842
+ if (assignment.agent === 'CODEX') {
2843
+ agentResult = await runCodex({
2014
2844
  workspaceDir,
2015
2845
  binDir,
2016
2846
  eventDir,
@@ -2026,8 +2856,10 @@ async function startWorker(options = {}) {
2026
2856
  onChild: (controls) => {
2027
2857
  activeTerminators.set(task.id, controls.terminate);
2028
2858
  },
2029
- })
2030
- : await runClaude({
2859
+ });
2860
+ }
2861
+ else if (assignment.agent === 'CLAUDE_CODE') {
2862
+ agentResult = await runClaude({
2031
2863
  workspaceDir,
2032
2864
  binDir,
2033
2865
  eventDir,
@@ -2045,6 +2877,27 @@ async function startWorker(options = {}) {
2045
2877
  activeTerminators.set(task.id, controls.terminate);
2046
2878
  },
2047
2879
  });
2880
+ }
2881
+ else if (assignment.agent === 'CURSOR_COMPOSER') {
2882
+ const assignmentAgent = assignment.agent;
2883
+ const assignmentModel = assignment.model;
2884
+ const plannedAttemptIndex = assignment.attemptPlan?.options.findIndex((option) => option.agent === assignmentAgent && option.model === assignmentModel);
2885
+ const attemptIndex = typeof plannedAttemptIndex === 'number' && plannedAttemptIndex >= 0
2886
+ ? plannedAttemptIndex
2887
+ : 0;
2888
+ await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
2889
+ workerKey,
2890
+ leaseToken,
2891
+ attemptIndex,
2892
+ reason: 'unavailable_runtime',
2893
+ message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
2894
+ });
2895
+ fenced = true;
2896
+ throw new Error('worker_cursor_composer_runner_not_configured');
2897
+ }
2898
+ else {
2899
+ throw new Error(`unsupported_worker_agent:${assignment.agent}`);
2900
+ }
2048
2901
  agentCompletedAt = new Date();
2049
2902
  await queueEventDrain().catch((error) => {
2050
2903
  handleEventDrainFailure(error);
@@ -2238,7 +3091,7 @@ async function startWorker(options = {}) {
2238
3091
  };
2239
3092
  try {
2240
3093
  let claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
2241
- while (!shuttingDown) {
3094
+ while (!shuttingDown && !quiescingForUpdate) {
2242
3095
  if (fatalTaskError) {
2243
3096
  throw fatalTaskError;
2244
3097
  }
@@ -2261,6 +3114,7 @@ async function startWorker(options = {}) {
2261
3114
  capabilities,
2262
3115
  runningTaskCount: activeTaskRuns.size,
2263
3116
  }));
3117
+ handleWorkerAction(claim);
2264
3118
  claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
2265
3119
  }
2266
3120
  catch (error) {
@@ -2274,7 +3128,7 @@ async function startWorker(options = {}) {
2274
3128
  }
2275
3129
  const task = claim.task;
2276
3130
  if (!task) {
2277
- if (shuttingDown) {
3131
+ if (shuttingDown || quiescingForUpdate) {
2278
3132
  break;
2279
3133
  }
2280
3134
  if (options.once) {
@@ -2312,6 +3166,10 @@ async function startWorker(options = {}) {
2312
3166
  if (sessionExpired) {
2313
3167
  throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
2314
3168
  }
3169
+ if (quiescingForUpdate) {
3170
+ console.log('Worker quiesced for managed update. Exiting for supervisor restart.');
3171
+ return;
3172
+ }
2315
3173
  if (shuttingDown) {
2316
3174
  await sendWorkerHealthAlertSafely({
2317
3175
  state: 'stopped',
@@ -2340,6 +3198,10 @@ async function startWorker(options = {}) {
2340
3198
  }
2341
3199
  node_process_1.default.off('SIGINT', signalHandler);
2342
3200
  node_process_1.default.off('SIGTERM', signalHandler);
3201
+ if (options.superviseStdin) {
3202
+ node_process_1.default.stdin.off('end', stdinShutdownHandler);
3203
+ node_process_1.default.stdin.off('close', stdinShutdownHandler);
3204
+ }
2343
3205
  if (!crashed) {
2344
3206
  await sendWorkerHealthAlertSafely({
2345
3207
  state: 'stopped',
@@ -2348,6 +3210,7 @@ async function startWorker(options = {}) {
2348
3210
  taskId: firstActiveTaskId(),
2349
3211
  });
2350
3212
  }
3213
+ clearWorkerRuntimeState(target);
2351
3214
  }
2352
3215
  }, { env: options.env });
2353
3216
  }