@playdrop/playdrop-cli 0.12.42 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/index.d.ts +2 -0
  3. package/dist/apps/index.js +2 -1
  4. package/dist/apps/loadCheck.d.ts +4 -1
  5. package/dist/apps/loadCheck.js +40 -10
  6. package/dist/apps/staticHtml.d.ts +28 -0
  7. package/dist/apps/staticHtml.js +343 -0
  8. package/dist/apps/validate.js +2 -1
  9. package/dist/commands/captureListing.d.ts +1 -1
  10. package/dist/commands/captureListing.js +4 -3
  11. package/dist/commands/devServer.js +19 -0
  12. package/dist/commands/feedback.d.ts +2 -0
  13. package/dist/commands/feedback.js +44 -2
  14. package/dist/commands/upload.d.ts +24 -0
  15. package/dist/commands/upload.js +122 -4
  16. package/dist/commands/worker/instrument-evidence.js +1 -1
  17. package/dist/commands/worker/runtime.d.ts +15 -0
  18. package/dist/commands/worker/runtime.js +121 -11
  19. package/dist/commands/worker.d.ts +28 -2
  20. package/dist/commands/worker.js +843 -170
  21. package/dist/index.js +7 -0
  22. package/dist/listingPreflight.js +15 -4
  23. package/dist/recorderRelease.d.ts +18 -0
  24. package/dist/recorderRelease.js +90 -0
  25. package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -2
  26. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +3 -2
  28. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  29. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +13 -2
  30. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  31. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  32. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts +2 -1
  33. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts.map +1 -1
  34. package/node_modules/@playdrop/api-client/dist/domains/me.js +11 -0
  35. package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -3
  36. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  37. package/node_modules/@playdrop/api-client/dist/index.js +14 -2
  38. package/node_modules/@playdrop/config/client-meta.json +2 -2
  39. package/node_modules/@playdrop/config/dist/src/constants.d.ts +1 -0
  40. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  41. package/node_modules/@playdrop/config/dist/src/constants.js +1 -0
  42. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  43. package/node_modules/@playdrop/types/dist/api.d.ts +195 -4
  44. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  45. package/node_modules/@playdrop/types/dist/api.js +25 -2
  46. package/node_modules/entities/decode-codepoint.d.ts +19 -0
  47. package/node_modules/entities/decode-codepoint.d.ts.map +1 -0
  48. package/node_modules/entities/decode-codepoint.js +77 -0
  49. package/node_modules/entities/decode-codepoint.js.map +1 -0
  50. package/node_modules/entities/decode.d.ts +209 -0
  51. package/node_modules/entities/decode.d.ts.map +1 -0
  52. package/node_modules/entities/decode.js +511 -0
  53. package/node_modules/entities/decode.js.map +1 -0
  54. package/node_modules/entities/encode.d.ts +22 -0
  55. package/node_modules/entities/encode.d.ts.map +1 -0
  56. package/node_modules/entities/encode.js +73 -0
  57. package/node_modules/entities/encode.js.map +1 -0
  58. package/node_modules/entities/escape.d.ts +43 -0
  59. package/node_modules/entities/escape.d.ts.map +1 -0
  60. package/node_modules/entities/escape.js +121 -0
  61. package/node_modules/entities/escape.js.map +1 -0
  62. package/node_modules/entities/generated/decode-data-html.d.ts +2 -0
  63. package/node_modules/entities/generated/decode-data-html.d.ts.map +1 -0
  64. package/node_modules/entities/generated/decode-data-html.js +10 -0
  65. package/node_modules/entities/generated/decode-data-html.js.map +1 -0
  66. package/node_modules/entities/generated/decode-data-xml.d.ts +2 -0
  67. package/node_modules/entities/generated/decode-data-xml.d.ts.map +1 -0
  68. package/node_modules/entities/generated/decode-data-xml.js +10 -0
  69. package/node_modules/entities/generated/decode-data-xml.js.map +1 -0
  70. package/node_modules/entities/generated/encode-html.d.ts +8 -0
  71. package/node_modules/entities/generated/encode-html.d.ts.map +1 -0
  72. package/node_modules/entities/generated/encode-html.js +13 -0
  73. package/node_modules/entities/generated/encode-html.js.map +1 -0
  74. package/node_modules/entities/index.d.ts +96 -0
  75. package/node_modules/entities/index.d.ts.map +1 -0
  76. package/node_modules/entities/index.js +131 -0
  77. package/node_modules/entities/index.js.map +1 -0
  78. package/node_modules/entities/package.json +3 -0
  79. package/node_modules/parse5/common/doctype.d.ts +4 -0
  80. package/node_modules/parse5/common/doctype.js +118 -0
  81. package/node_modules/parse5/common/error-codes.d.ts +67 -0
  82. package/node_modules/parse5/common/error-codes.js +66 -0
  83. package/node_modules/parse5/common/foreign-content.d.ts +9 -0
  84. package/node_modules/parse5/common/foreign-content.js +237 -0
  85. package/node_modules/parse5/common/html.d.ts +289 -0
  86. package/node_modules/parse5/common/html.js +528 -0
  87. package/node_modules/parse5/common/token.d.ts +84 -0
  88. package/node_modules/parse5/common/token.js +24 -0
  89. package/node_modules/parse5/common/unicode.d.ts +42 -0
  90. package/node_modules/parse5/common/unicode.js +70 -0
  91. package/node_modules/parse5/index.d.ts +71 -0
  92. package/node_modules/parse5/index.js +54 -0
  93. package/node_modules/parse5/package.json +1 -0
  94. package/node_modules/parse5/parser/formatting-element-list.d.ts +36 -0
  95. package/node_modules/parse5/parser/formatting-element-list.js +114 -0
  96. package/node_modules/parse5/parser/index.d.ts +221 -0
  97. package/node_modules/parse5/parser/index.js +3240 -0
  98. package/node_modules/parse5/parser/open-element-stack.d.ts +53 -0
  99. package/node_modules/parse5/parser/open-element-stack.js +328 -0
  100. package/node_modules/parse5/serializer/index.d.ts +60 -0
  101. package/node_modules/parse5/serializer/index.js +172 -0
  102. package/node_modules/parse5/tokenizer/index.d.ts +247 -0
  103. package/node_modules/parse5/tokenizer/index.js +2714 -0
  104. package/node_modules/parse5/tokenizer/preprocessor.d.ts +36 -0
  105. package/node_modules/parse5/tokenizer/preprocessor.js +200 -0
  106. package/node_modules/parse5/tree-adapters/default.d.ts +84 -0
  107. package/node_modules/parse5/tree-adapters/default.js +176 -0
  108. package/node_modules/parse5/tree-adapters/interface.d.ts +255 -0
  109. package/node_modules/parse5/tree-adapters/interface.js +2 -0
  110. package/package.json +8 -1
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readEnvBoolean = exports.readCodexSandboxMode = exports.readCodexRolloutTokenUsageForThread = exports.extractCodexTokensUsed = exports.extractCodexThreadId = exports.extractCodexRolloutTokenUsage = exports.extractAgentTokenUsage = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = exports.buildWorkerChildEnv = exports.buildPlaywrightMcpCommand = exports.buildCodexExecArgs = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.assertWorkerTokenUsageWithinCap = void 0;
6
+ exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readEnvBoolean = exports.readCodexSandboxMode = exports.readCodexRolloutTokenUsageForThread = exports.extractCodexTokensUsed = exports.extractCodexThreadId = exports.extractClaudeSessionId = exports.extractCodexRolloutTokenUsage = exports.extractAgentTokenUsage = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS = exports.DEFAULT_CREATOR_AGENT_TIMEOUT_MS = exports.DEFAULT_CODEX_TIMEOUT_MS = exports.buildWorkerChildEnv = exports.buildPlaywrightMcpCommand = exports.buildCodexExecArgs = exports.buildCleanRoomChildEnv = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.assertWorkerTokenUsageWithinCap = void 0;
7
+ exports.usesCreatorRecovery = usesCreatorRecovery;
8
+ exports.buildCreatorRecoveryPrompt = buildCreatorRecoveryPrompt;
9
+ exports.buildGameEvalRetryPrompt = buildGameEvalRetryPrompt;
7
10
  exports.allocateWorkerDevPort = allocateWorkerDevPort;
8
11
  exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
9
12
  exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
@@ -12,6 +15,7 @@ exports.nextClaimBackoffMs = nextClaimBackoffMs;
12
15
  exports.claimBackoffDelayMs = claimBackoffDelayMs;
13
16
  exports.resolveWorkerHomeDir = resolveWorkerHomeDir;
14
17
  exports.pruneWorkerTaskWorkspaces = pruneWorkerTaskWorkspaces;
18
+ exports.prepareClaimedTaskWorkspace = prepareClaimedTaskWorkspace;
15
19
  exports.resolveWorkerInstrumentSurface = resolveWorkerInstrumentSurface;
16
20
  exports.hasAgentTaskUploadedArtifact = hasAgentTaskUploadedArtifact;
17
21
  exports.appendWorkerTranscriptChunks = appendWorkerTranscriptChunks;
@@ -19,6 +23,7 @@ exports.readTaskNextStepsFile = readTaskNextStepsFile;
19
23
  exports.assertTaskUploadResultMatchesContext = assertTaskUploadResultMatchesContext;
20
24
  exports.stageAssignmentWorkspaceV2 = stageAssignmentWorkspaceV2;
21
25
  exports.assertAssignmentPluginBundleExtractionBounds = assertAssignmentPluginBundleExtractionBounds;
26
+ exports.stageDevPluginWorkingTree = stageDevPluginWorkingTree;
22
27
  exports.stageAssignmentPluginBundle = stageAssignmentPluginBundle;
23
28
  exports.resolveDevPluginWorkingTree = resolveDevPluginWorkingTree;
24
29
  exports.discoverWorkerProjectRoot = discoverWorkerProjectRoot;
@@ -43,6 +48,7 @@ exports.prepareCodexRunHomes = prepareCodexRunHomes;
43
48
  exports.prepareClaudeRunConfig = prepareClaudeRunConfig;
44
49
  exports.removeAgentRunCredentials = removeAgentRunCredentials;
45
50
  exports.removeStaleAgentRunCredentials = removeStaleAgentRunCredentials;
51
+ exports.mergeCreatorRecoveryResults = mergeCreatorRecoveryResults;
46
52
  exports.buildAgentFailureCode = buildAgentFailureCode;
47
53
  exports.describeAgentFailureForEvent = describeAgentFailureForEvent;
48
54
  exports.createLocalPlaydropShim = createLocalPlaydropShim;
@@ -50,6 +56,7 @@ exports.parseCodexModelCatalog = parseCodexModelCatalog;
50
56
  exports.parseClaudeModelCatalog = parseClaudeModelCatalog;
51
57
  exports.claudeModelProbeReportsSelection = claudeModelProbeReportsSelection;
52
58
  exports.probeWorkerProviderIndependently = probeWorkerProviderIndependently;
59
+ exports.describeWorkerPreflightReason = describeWorkerPreflightReason;
53
60
  exports.buildWorkerSetupActions = buildWorkerSetupActions;
54
61
  exports.resolveWorkerStateDir = resolveWorkerStateDir;
55
62
  exports.resolveWorkerSupervisorLockFile = resolveWorkerSupervisorLockFile;
@@ -71,6 +78,7 @@ exports.showWorkerStatus = showWorkerStatus;
71
78
  exports.setupWorker = setupWorker;
72
79
  exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
73
80
  exports.enforceInstrumentBrowserHygiene = enforceInstrumentBrowserHygiene;
81
+ exports.assignmentRequiresRegisteredBundle = assignmentRequiresRegisteredBundle;
74
82
  exports.startWorker = startWorker;
75
83
  exports.reportTask = reportTask;
76
84
  exports.showTaskContext = showTaskContext;
@@ -101,6 +109,7 @@ const commandContext_1 = require("../commandContext");
101
109
  const config_1 = require("../config");
102
110
  const messages_1 = require("../messages");
103
111
  const output_1 = require("../output");
112
+ const recorderRelease_1 = require("../recorderRelease");
104
113
  const shellProbe_1 = require("../shellProbe");
105
114
  const review_1 = require("./review");
106
115
  const upload_1 = require("./upload");
@@ -110,13 +119,17 @@ var runtime_2 = require("./worker/runtime");
110
119
  Object.defineProperty(exports, "assertWorkerTokenUsageWithinCap", { enumerable: true, get: function () { return runtime_2.assertWorkerTokenUsageWithinCap; } });
111
120
  Object.defineProperty(exports, "buildClaudeExecArgs", { enumerable: true, get: function () { return runtime_2.buildClaudeExecArgs; } });
112
121
  Object.defineProperty(exports, "buildClaudePermissionSettings", { enumerable: true, get: function () { return runtime_2.buildClaudePermissionSettings; } });
122
+ Object.defineProperty(exports, "buildCleanRoomChildEnv", { enumerable: true, get: function () { return runtime_2.buildCleanRoomChildEnv; } });
113
123
  Object.defineProperty(exports, "buildCodexExecArgs", { enumerable: true, get: function () { return runtime_2.buildCodexExecArgs; } });
114
124
  Object.defineProperty(exports, "buildPlaywrightMcpCommand", { enumerable: true, get: function () { return runtime_2.buildPlaywrightMcpCommand; } });
115
125
  Object.defineProperty(exports, "buildWorkerChildEnv", { enumerable: true, get: function () { return runtime_2.buildWorkerChildEnv; } });
116
126
  Object.defineProperty(exports, "DEFAULT_CODEX_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CODEX_TIMEOUT_MS; } });
127
+ Object.defineProperty(exports, "DEFAULT_CREATOR_AGENT_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CREATOR_AGENT_TIMEOUT_MS; } });
128
+ Object.defineProperty(exports, "DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS; } });
117
129
  Object.defineProperty(exports, "DEFAULT_WORKER_TOKEN_CAP", { enumerable: true, get: function () { return runtime_2.DEFAULT_WORKER_TOKEN_CAP; } });
118
130
  Object.defineProperty(exports, "extractAgentTokenUsage", { enumerable: true, get: function () { return runtime_2.extractAgentTokenUsage; } });
119
131
  Object.defineProperty(exports, "extractCodexRolloutTokenUsage", { enumerable: true, get: function () { return runtime_2.extractCodexRolloutTokenUsage; } });
132
+ Object.defineProperty(exports, "extractClaudeSessionId", { enumerable: true, get: function () { return runtime_2.extractClaudeSessionId; } });
120
133
  Object.defineProperty(exports, "extractCodexThreadId", { enumerable: true, get: function () { return runtime_2.extractCodexThreadId; } });
121
134
  Object.defineProperty(exports, "extractCodexTokensUsed", { enumerable: true, get: function () { return runtime_2.extractCodexTokensUsed; } });
122
135
  Object.defineProperty(exports, "readCodexRolloutTokenUsageForThread", { enumerable: true, get: function () { return runtime_2.readCodexRolloutTokenUsageForThread; } });
@@ -137,7 +150,9 @@ const FAIL_REPORT_RETRY_DELAY_MS = 2000;
137
150
  const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
138
151
  const SLACK_API_BASE_URL = 'https://slack.com/api';
139
152
  const WORKER_WRAPPER_CONTRACT_VERSION = 2;
140
- const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
153
+ const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'STATIC_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
154
+ const GAME_CREATION_TASK_KINDS = new Set(['NEW_GAME', 'STATIC_GAME', 'REMIX_GAME', 'GAME_UPDATE']);
155
+ const AGENT_MANAGED_UPLOAD_TASK_KINDS = new Set(['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE']);
141
156
  const requireFromWorker = (0, node_module_1.createRequire)(__filename);
142
157
  const AGENT_BUNDLE_MANIFEST_PATH = 'agent-bundle.json';
143
158
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES = 1000;
@@ -148,6 +163,36 @@ const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
148
163
  const ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES = 22;
149
164
  const ZIP_MAX_COMMENT_BYTES = 0xffff;
150
165
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
166
+ function usesCreatorRecovery(kind) {
167
+ return GAME_CREATION_TASK_KINDS.has(kind);
168
+ }
169
+ function buildCreatorRecoveryPrompt(input) {
170
+ const prompt = input.prompt.trim();
171
+ if (!prompt) {
172
+ throw new Error('agent_task_assignment_prompt_missing');
173
+ }
174
+ if (!GAME_CREATION_TASK_KINDS.has(input.kind)) {
175
+ throw new Error(`agent_task_creation_recovery_unsupported:${input.kind}`);
176
+ }
177
+ const finishInstruction = AGENT_MANAGED_UPLOAD_TASK_KINDS.has(input.kind)
178
+ ? 'Prioritize `playdrop task upload` and then `playdrop task done`.'
179
+ : 'Finish a valid standalone `index.html` at the project root, then stop so the worker can publish it.';
180
+ return [
181
+ 'This task was paused because it was taking too long. Continue from the existing workspace and do not restart or redesign the game.',
182
+ 'You have 10 minutes to finish the strongest solid version already present. Make only the smallest fixes required for validation and publishing.',
183
+ `${finishInstruction} Do not add features or polish. If completion cannot succeed, report the exact blocker and stop.`,
184
+ `Original request: ${prompt}`,
185
+ ].join('\n\n');
186
+ }
187
+ function buildGameEvalRetryPrompt(prompt) {
188
+ return [
189
+ prompt,
190
+ '',
191
+ 'This is a retry in the existing evaluation workspace.',
192
+ 'Reuse the existing result.json and evidence. Do not replay completed game checks unless required evidence is missing or invalid.',
193
+ 'First correct any deterministic submission-contract error, then submit the existing result and evidence and finish.',
194
+ ].join('\n');
195
+ }
151
196
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
152
197
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
153
198
  ['task', 'context'],
@@ -505,6 +550,29 @@ async function pruneWorkerTaskWorkspaces(incomingWorkspaceDir, retention = expor
505
550
  }
506
551
  return removed;
507
552
  }
553
+ async function prepareClaimedTaskWorkspace(input) {
554
+ await pruneWorkerTaskWorkspaces(input.workspaceDir);
555
+ const resumed = input.attempt > 1 && (0, node_fs_1.existsSync)(input.workspaceDir);
556
+ if (!resumed) {
557
+ await (0, promises_1.rm)(input.workspaceDir, { recursive: true, force: true });
558
+ await (0, promises_1.mkdir)(input.workspaceDir, { recursive: true });
559
+ return { resumed: false };
560
+ }
561
+ await removeAgentRunCredentials(input.workspaceDir);
562
+ const retainedProjectBindings = [
563
+ node_path_1.default.join(input.workspaceDir, '.playdrop.json'),
564
+ ...(0, node_fs_1.readdirSync)(input.workspaceDir, { withFileTypes: true })
565
+ .filter((entry) => entry.isDirectory() && entry.name !== '.playdrop')
566
+ .map((entry) => node_path_1.default.join(input.workspaceDir, entry.name, '.playdrop.json')),
567
+ ];
568
+ await Promise.all([
569
+ (0, promises_1.rm)(node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-events'), { recursive: true, force: true }),
570
+ (0, promises_1.rm)(node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-upload.lock'), { force: true }),
571
+ (0, promises_1.rm)(node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-upload-result.json'), { force: true }),
572
+ ...retainedProjectBindings.map((file) => (0, promises_1.rm)(file, { force: true })),
573
+ ]);
574
+ return { resumed: true };
575
+ }
508
576
  function buildWorkerTaskContext(input) {
509
577
  const playdrop = input.metadata.playdrop;
510
578
  if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
@@ -512,7 +580,7 @@ function buildWorkerTaskContext(input) {
512
580
  }
513
581
  const metadata = playdrop;
514
582
  const kind = metadata.kind;
515
- if (kind !== 'NEW_GAME' && kind !== 'REMIX_GAME' && kind !== 'GAME_UPDATE' && kind !== 'GAME_REVIEW' && kind !== 'GAME_EVAL') {
583
+ if (kind !== 'NEW_GAME' && kind !== 'STATIC_GAME' && kind !== 'REMIX_GAME' && kind !== 'GAME_UPDATE' && kind !== 'GAME_REVIEW' && kind !== 'GAME_EVAL') {
516
584
  throw new Error('agent_task_context_kind_missing');
517
585
  }
518
586
  const creatorUsername = typeof metadata.creatorUsername === 'string'
@@ -583,6 +651,23 @@ function resolveWorkerInstrumentSurface(taskContext) {
583
651
  return null;
584
652
  }
585
653
  const metadata = playdrop;
654
+ if (taskContext.kind === 'STATIC_GAME') {
655
+ const surfaceContext = metadata.surfaceContext;
656
+ if (!surfaceContext || typeof surfaceContext !== 'object' || Array.isArray(surfaceContext)) {
657
+ return null;
658
+ }
659
+ const context = surfaceContext;
660
+ if (context.deviceClass === 'DESKTOP') {
661
+ return 'DESKTOP';
662
+ }
663
+ if (context.orientation === 'PORTRAIT') {
664
+ return 'MOBILE_PORTRAIT';
665
+ }
666
+ if (context.orientation === 'LANDSCAPE') {
667
+ return 'MOBILE_LANDSCAPE';
668
+ }
669
+ return null;
670
+ }
586
671
  if (taskContext.kind === 'GAME_REVIEW') {
587
672
  const reviewTarget = metadata.reviewTarget;
588
673
  if (!reviewTarget || typeof reviewTarget !== 'object' || Array.isArray(reviewTarget)) {
@@ -1073,17 +1158,19 @@ function readAgentBundleManifest(extractedRoot) {
1073
1158
  }
1074
1159
  const manifest = parsed;
1075
1160
  if (manifest.version !== 1
1076
- || !manifest.plugin
1077
- || typeof manifest.plugin.root !== 'string'
1161
+ || !Object.prototype.hasOwnProperty.call(manifest, 'plugin')
1162
+ || (manifest.plugin !== null && typeof manifest.plugin?.root !== 'string')
1078
1163
  || !manifest.workspace
1079
1164
  || !Array.isArray(manifest.workspace.files)) {
1080
1165
  throw new Error('agent_bundle_manifest_invalid');
1081
1166
  }
1082
- const pluginRoot = normalizeAgentBundlePath(extractedRoot, manifest.plugin.root, 'agent_bundle_plugin_root_invalid');
1083
- if (!isFile(node_path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json'))) {
1167
+ const pluginRoot = manifest.plugin === null
1168
+ ? null
1169
+ : normalizeAgentBundlePath(extractedRoot, manifest.plugin?.root, 'agent_bundle_plugin_root_invalid');
1170
+ if (pluginRoot && !isFile(node_path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json'))) {
1084
1171
  throw new Error('agent_bundle_codex_plugin_manifest_missing');
1085
1172
  }
1086
- if (!isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))) {
1173
+ if (pluginRoot && !isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))) {
1087
1174
  throw new Error('agent_bundle_claude_plugin_manifest_missing');
1088
1175
  }
1089
1176
  const destinations = new Set();
@@ -1146,6 +1233,19 @@ async function stageAgentBundleWorkspaceFiles(extractedRoot, workspaceDir, taskK
1146
1233
  }
1147
1234
  return resolved.pluginRoot;
1148
1235
  }
1236
+ async function stageDevPluginWorkingTree(input) {
1237
+ const sourceRoot = node_path_1.default.resolve(input.workingTree);
1238
+ readAgentBundleManifest(sourceRoot);
1239
+ const snapshotRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'dev-task-plugin');
1240
+ await (0, promises_1.rm)(snapshotRoot, { recursive: true, force: true });
1241
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(snapshotRoot), { recursive: true });
1242
+ await (0, promises_1.cp)(sourceRoot, snapshotRoot, { recursive: true });
1243
+ const pluginRoot = await stageAgentBundleWorkspaceFiles(snapshotRoot, input.workspaceDir, input.taskKind);
1244
+ if (!pluginRoot) {
1245
+ throw new Error('worker_dev_plugin_working_tree_requires_plugin');
1246
+ }
1247
+ return pluginRoot;
1248
+ }
1149
1249
  async function stageAssignmentPluginBundle(input) {
1150
1250
  const sha256 = typeof input.pluginBundle.sha256 === 'string' ? input.pluginBundle.sha256.trim() : '';
1151
1251
  const fileName = typeof input.pluginBundle.fileName === 'string' ? input.pluginBundle.fileName.trim() : '';
@@ -1168,7 +1268,7 @@ async function stageAssignmentPluginBundle(input) {
1168
1268
  throw new Error('agent_task_plugin_bundle_sha_mismatch');
1169
1269
  }
1170
1270
  assertAssignmentPluginBundleExtractionBounds(buffer);
1171
- const pluginRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-plugin');
1271
+ const pluginRoot = node_path_1.default.join(input.extractionDir ?? input.workspaceDir, '.playdrop', 'task-plugin');
1172
1272
  await (0, promises_1.rm)(pluginRoot, { recursive: true, force: true });
1173
1273
  await (0, promises_1.mkdir)(pluginRoot, { recursive: true });
1174
1274
  const entries = (0, fflate_1.unzipSync)(new Uint8Array(buffer));
@@ -1215,7 +1315,9 @@ function resolveDevPluginWorkingTree(input) {
1215
1315
  throw new Error(`worker_dev_plugin_working_tree_requires_dev_environment:${input.env}`);
1216
1316
  }
1217
1317
  const resolved = node_path_1.default.resolve(workingTree);
1218
- readAgentBundleManifest(resolved);
1318
+ if (!readAgentBundleManifest(resolved).pluginRoot) {
1319
+ throw new Error('worker_dev_plugin_working_tree_requires_plugin');
1320
+ }
1219
1321
  return resolved;
1220
1322
  }
1221
1323
  // A PlayDrop project root is the topmost directory holding a catalogue.json,
@@ -1448,14 +1550,6 @@ function readCodexAuthenticated(homeDir = node_os_1.default.homedir()) {
1448
1550
  return false;
1449
1551
  }
1450
1552
  }
1451
- function probeNpmVersion() {
1452
- const result = (0, shellProbe_1.runShell)('npm --version');
1453
- const version = result.output.trim().split('\n')[0]?.trim() ?? '';
1454
- if (result.exitCode !== 0 || !version) {
1455
- throw new Error(`worker_preflight_npm_missing: "npm --version" exited with code ${result.exitCode}.`);
1456
- }
1457
- return version;
1458
- }
1459
1553
  function probePlaywrightChromium() {
1460
1554
  let playwrightCore;
1461
1555
  let packageJson;
@@ -1900,11 +1994,15 @@ async function prepareCodexRunHomes(input) {
1900
1994
  const runRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'codex-run');
1901
1995
  const homeDir = node_path_1.default.join(runRoot, 'home');
1902
1996
  const codexHomeDir = node_path_1.default.join(runRoot, 'codex-home');
1903
- await (0, promises_1.rm)(runRoot, { recursive: true, force: true });
1997
+ if (!input.preserveExisting) {
1998
+ await (0, promises_1.rm)(runRoot, { recursive: true, force: true });
1999
+ }
1904
2000
  await (0, promises_1.mkdir)(runRoot, { recursive: true, mode: 0o700 });
1905
2001
  await (0, promises_1.chmod)(runRoot, 0o700);
1906
- await (0, promises_1.mkdir)(homeDir, { mode: 0o700 });
1907
- await (0, promises_1.mkdir)(codexHomeDir, { mode: 0o700 });
2002
+ await (0, promises_1.mkdir)(homeDir, { recursive: true, mode: 0o700 });
2003
+ await (0, promises_1.chmod)(homeDir, 0o700);
2004
+ await (0, promises_1.mkdir)(codexHomeDir, { recursive: true, mode: 0o700 });
2005
+ await (0, promises_1.chmod)(codexHomeDir, 0o700);
1908
2006
  const sourceCodexHome = node_path_1.default.resolve(input.sourceCodexHome?.trim()
1909
2007
  || node_process_1.default.env.CODEX_HOME?.trim()
1910
2008
  || node_path_1.default.join(node_os_1.default.homedir(), '.codex'));
@@ -1915,12 +2013,14 @@ async function prepareCodexRunHomes(input) {
1915
2013
  const stagedAuthPath = node_path_1.default.join(codexHomeDir, 'auth.json');
1916
2014
  await (0, promises_1.copyFile)(sourceAuthPath, stagedAuthPath);
1917
2015
  await (0, promises_1.chmod)(stagedAuthPath, 0o600);
1918
- await stageWorkerPlaydropSession({
1919
- homeDir,
1920
- workerUsername: input.workerUsername,
1921
- envName: input.envName,
1922
- playdropConfig: input.playdropConfig,
1923
- });
2016
+ if (input.stagePlaydropSession !== false) {
2017
+ await stageWorkerPlaydropSession({
2018
+ homeDir,
2019
+ workerUsername: input.workerUsername,
2020
+ envName: input.envName,
2021
+ playdropConfig: input.playdropConfig,
2022
+ });
2023
+ }
1924
2024
  return { homeDir, codexHomeDir };
1925
2025
  }
1926
2026
  async function stageWorkerPlaydropSession(input) {
@@ -1999,11 +2099,13 @@ async function removeStaleAgentRunCredentials(workerHomeDir = resolveWorkerHomeD
1999
2099
  }
2000
2100
  async function runCodex(input) {
2001
2101
  const { homeDir, codexHomeDir } = await prepareCodexRunHomes({
2002
- workspaceDir: input.workspaceDir,
2102
+ workspaceDir: input.stateDir ?? input.workspaceDir,
2003
2103
  workerUsername: input.workerUsername,
2004
2104
  envName: input.envName,
2105
+ stagePlaydropSession: !input.cleanRoom,
2106
+ preserveExisting: Boolean(input.resumeSessionId),
2005
2107
  });
2006
- if (input.taskPluginRoot) {
2108
+ if (input.taskPluginRoot && !input.resumeSessionId) {
2007
2109
  const pluginEnv = {
2008
2110
  ...node_process_1.default.env,
2009
2111
  HOME: homeDir,
@@ -2018,7 +2120,7 @@ async function runCodex(input) {
2018
2120
  env: pluginEnv,
2019
2121
  });
2020
2122
  }
2021
- if (input.enablePlaywrightMcp) {
2123
+ if (input.enablePlaywrightMcp && !input.resumeSessionId) {
2022
2124
  const server = (0, runtime_1.buildPlaywrightMcpCommand)(input.instrumentSurface);
2023
2125
  await execFileAsync('codex', ['mcp', 'add', 'playwright', '--', server.command, ...server.args], {
2024
2126
  cwd: input.workspaceDir,
@@ -2040,12 +2142,17 @@ async function runCodex(input) {
2040
2142
  workspaceDir: input.workspaceDir,
2041
2143
  model: input.codexModel.model,
2042
2144
  reasoningEffort: input.codexModel.reasoningEffort,
2043
- networkAccess: (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_AGENT_NETWORK_ACCESS', false),
2145
+ networkAccess: input.cleanRoom || (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_AGENT_NETWORK_ACCESS', false),
2044
2146
  sandboxMode: (0, runtime_1.readCodexSandboxMode)(),
2045
2147
  imagePaths: input.imagePaths,
2148
+ resumeSessionId: input.resumeSessionId,
2046
2149
  }),
2047
2150
  cwd: input.workspaceDir,
2048
- env: (0, runtime_1.buildWorkerChildEnv)({
2151
+ env: input.cleanRoom ? (0, runtime_1.buildCleanRoomChildEnv)({
2152
+ binDir: input.binDir,
2153
+ homeDir,
2154
+ codexHomeDir,
2155
+ }) : (0, runtime_1.buildWorkerChildEnv)({
2049
2156
  binDir: input.binDir,
2050
2157
  taskId: input.taskId,
2051
2158
  taskToken: input.taskToken,
@@ -2058,7 +2165,7 @@ async function runCodex(input) {
2058
2165
  playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
2059
2166
  }),
2060
2167
  stdin: input.prompt,
2061
- timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CODEX_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2168
+ timeoutMs: input.timeoutMs ?? (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CODEX_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2062
2169
  maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
2063
2170
  onTranscriptChunks: async (chunks) => {
2064
2171
  if (!observedCodexThreadId) {
@@ -2080,16 +2187,17 @@ async function runCodex(input) {
2080
2187
  if (result.tokenUsage.usageParseError) {
2081
2188
  console.warn('Codex thread id was not observed; rollout token usage could not be read.');
2082
2189
  }
2083
- return result;
2190
+ return { ...result, providerSessionId: null };
2084
2191
  }
2085
2192
  const rolloutUsage = await (0, runtime_1.readCodexRolloutTokenUsageForThread)(observedCodexThreadId, {
2086
2193
  sessionsRoot: node_path_1.default.join(codexHomeDir, 'sessions'),
2194
+ deleteAfterRead: false,
2087
2195
  });
2088
2196
  if (rolloutUsage.usageParseError || rolloutUsage.totalTokens === null) {
2089
2197
  if (result.tokenUsage.usageParseError) {
2090
2198
  console.warn(`Codex rollout token usage could not be read for thread ${observedCodexThreadId}: ${rolloutUsage.usageParseError ?? 'unknown_error'}.`);
2091
2199
  }
2092
- return result;
2200
+ return { ...result, providerSessionId: observedCodexThreadId };
2093
2201
  }
2094
2202
  if (!result.tokenUsage.usageParseError
2095
2203
  && result.tokenUsage.totalTokens !== null
@@ -2098,20 +2206,25 @@ async function runCodex(input) {
2098
2206
  }
2099
2207
  return {
2100
2208
  ...result,
2209
+ providerSessionId: observedCodexThreadId,
2101
2210
  tokenUsage: rolloutUsage,
2102
2211
  tokensUsed: rolloutUsage.totalTokens,
2103
2212
  };
2104
2213
  }
2105
2214
  async function runClaude(input) {
2106
- const { playdropConfigPath } = await prepareClaudeRunConfig({
2107
- workspaceDir: input.workspaceDir,
2108
- workerUsername: input.workerUsername,
2109
- envName: input.envName,
2110
- });
2215
+ const playdropConfigPath = input.cleanRoom
2216
+ ? null
2217
+ : (await prepareClaudeRunConfig({
2218
+ workspaceDir: input.stateDir ?? input.workspaceDir,
2219
+ workerUsername: input.workerUsername,
2220
+ envName: input.envName,
2221
+ })).playdropConfigPath;
2111
2222
  const denyReadRoots = resolveClaudeDenyReadRoots(input.workspaceDir);
2112
2223
  const screenshotCollector = input.enableChrome || input.enablePlaywrightMcp
2113
2224
  ? (0, instrument_evidence_1.createWorkerInstrumentScreenshotCollector)(input.workspaceDir)
2114
2225
  : null;
2226
+ let observedClaudeSessionId = input.resumeSessionId?.trim() || null;
2227
+ let claudeSessionProbe = '';
2115
2228
  let playwrightMcpConfigPath = null;
2116
2229
  if (input.enablePlaywrightMcp) {
2117
2230
  playwrightMcpConfigPath = node_path_1.default.join(input.workspaceDir, '.playdrop', 'claude-run', 'playwright-mcp.json');
@@ -2125,12 +2238,16 @@ async function runClaude(input) {
2125
2238
  workspaceDir: input.workspaceDir,
2126
2239
  denyReadRoots,
2127
2240
  pluginDir: input.taskPluginRoot,
2241
+ playdropCliEnabled: !input.cleanRoom,
2128
2242
  chrome: input.enableChrome,
2129
2243
  playwrightMcp: input.enablePlaywrightMcp,
2130
2244
  playwrightMcpConfigPath,
2245
+ resumeSessionId: input.resumeSessionId,
2131
2246
  }),
2132
2247
  cwd: input.workspaceDir,
2133
- env: (0, runtime_1.buildWorkerChildEnv)({
2248
+ env: input.cleanRoom ? (0, runtime_1.buildCleanRoomChildEnv)({
2249
+ binDir: input.binDir,
2250
+ }) : (0, runtime_1.buildWorkerChildEnv)({
2134
2251
  binDir: input.binDir,
2135
2252
  taskId: input.taskId,
2136
2253
  taskToken: input.taskToken,
@@ -2138,7 +2255,7 @@ async function runClaude(input) {
2138
2255
  envName: input.envName,
2139
2256
  eventDir: input.eventDir,
2140
2257
  devPort: input.devPort,
2141
- playdropConfigPath,
2258
+ playdropConfigPath: playdropConfigPath,
2142
2259
  playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
2143
2260
  }),
2144
2261
  stdin: `${JSON.stringify({
@@ -2158,9 +2275,15 @@ async function runClaude(input) {
2158
2275
  ],
2159
2276
  },
2160
2277
  })}\n`,
2161
- timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2278
+ timeoutMs: input.timeoutMs ?? (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2162
2279
  maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
2163
- onTranscriptChunks: input.onTranscriptChunks,
2280
+ onTranscriptChunks: async (chunks) => {
2281
+ if (!observedClaudeSessionId) {
2282
+ claudeSessionProbe = (claudeSessionProbe + chunks.map((chunk) => chunk.content).join('')).slice(-64000);
2283
+ observedClaudeSessionId = (0, runtime_1.extractClaudeSessionId)(claudeSessionProbe);
2284
+ }
2285
+ await input.onTranscriptChunks(chunks);
2286
+ },
2164
2287
  ...(screenshotCollector
2165
2288
  ? { onStdoutText: (text) => screenshotCollector.push(text) > 0 }
2166
2289
  : {}),
@@ -2168,7 +2291,9 @@ async function runClaude(input) {
2168
2291
  onChild: input.onChild,
2169
2292
  });
2170
2293
  screenshotCollector?.finish();
2171
- return result;
2294
+ observedClaudeSessionId = observedClaudeSessionId
2295
+ ?? (0, runtime_1.extractClaudeSessionId)(`${result.stdout}\n${result.stderr}\n${result.outputTail}`);
2296
+ return { ...result, providerSessionId: observedClaudeSessionId };
2172
2297
  }
2173
2298
  function isPathInside(parent, child) {
2174
2299
  const relative = node_path_1.default.relative(parent, child);
@@ -2199,6 +2324,35 @@ function resolveClaudeDenyReadRoots(workspaceDir) {
2199
2324
  }
2200
2325
  // Raw stdout/stderr tails stay out of task results on purpose: transcripts are
2201
2326
  // streamed separately and results can surface to creators (spec section 11).
2327
+ function mergeCreatorRecoveryResults(runtime, initial, recovery) {
2328
+ const sumNullable = (left, right) => (left === null && right === null ? null : (left ?? 0) + (right ?? 0));
2329
+ const tokenUsage = runtime === 'CODEX' && recovery.tokenUsage.totalTokens !== null
2330
+ ? recovery.tokenUsage
2331
+ : {
2332
+ inputTokens: sumNullable(initial.tokenUsage.inputTokens, recovery.tokenUsage.inputTokens),
2333
+ outputTokens: sumNullable(initial.tokenUsage.outputTokens, recovery.tokenUsage.outputTokens),
2334
+ cacheCreationInputTokens: sumNullable(initial.tokenUsage.cacheCreationInputTokens, recovery.tokenUsage.cacheCreationInputTokens),
2335
+ cacheReadInputTokens: sumNullable(initial.tokenUsage.cacheReadInputTokens, recovery.tokenUsage.cacheReadInputTokens),
2336
+ totalTokens: sumNullable(initial.tokenUsage.totalTokens, recovery.tokenUsage.totalTokens),
2337
+ rawProviderUsage: {
2338
+ initial: initial.tokenUsage.rawProviderUsage,
2339
+ recovery: recovery.tokenUsage.rawProviderUsage,
2340
+ },
2341
+ usageParseError: initial.tokenUsage.totalTokens === null || recovery.tokenUsage.totalTokens === null
2342
+ ? 'creator_recovery_usage_incomplete'
2343
+ : null,
2344
+ };
2345
+ const recoveryMarker = recovery.timedOut ? '\nagent_task_creation_recovery_timeout' : '';
2346
+ return {
2347
+ ...recovery,
2348
+ stdout: `${initial.stdout}\n${recovery.stdout}`.slice(-runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS),
2349
+ stderr: `${initial.stderr}\n${recovery.stderr}${recoveryMarker}`.slice(-runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS),
2350
+ outputTail: `${initial.outputTail}\n${recovery.outputTail}${recoveryMarker}`.slice(-runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS),
2351
+ tokenUsage,
2352
+ tokensUsed: tokenUsage.totalTokens,
2353
+ providerSessionId: recovery.providerSessionId ?? initial.providerSessionId ?? null,
2354
+ };
2355
+ }
2202
2356
  function buildAgentRunResult(result) {
2203
2357
  return {
2204
2358
  exitCode: result.exitCode,
@@ -2326,6 +2480,12 @@ function extractProviderStatusCode(text) {
2326
2480
  function buildAgentFailureCode(agent, result, request) {
2327
2481
  const provider = providerNameForAgent(agent);
2328
2482
  const combinedOutput = `${result.outputTail}\n${result.stderr}\n${result.stdout}`;
2483
+ if (combinedOutput.includes('agent_task_resume_session_missing')) {
2484
+ return `agent_task_resume_session_missing:${provider}`;
2485
+ }
2486
+ if (combinedOutput.includes('agent_task_creation_recovery_timeout')) {
2487
+ return `agent_task_creation_recovery_timeout:${provider}`;
2488
+ }
2329
2489
  if (request
2330
2490
  && (/\b(?:unknown|invalid|unsupported)\s+(?:model|effort)\b/i.test(combinedOutput)
2331
2491
  || /\b(?:model|effort)\b.{0,80}\b(?:not found|not available|does not exist|is unavailable|is not supported)\b/i.test(combinedOutput))) {
@@ -2363,6 +2523,12 @@ function describeAgentFailureForEvent(errorCode) {
2363
2523
  if (code === 'agent_provider_timeout') {
2364
2524
  return `${providerLabel} timed out before completing the task. Retry this task when the provider is healthy.`;
2365
2525
  }
2526
+ if (code === 'agent_task_resume_session_missing') {
2527
+ return `${providerLabel} timed out, but the worker could not recover the same conversation. The task was stopped without starting over.`;
2528
+ }
2529
+ if (code === 'agent_task_creation_recovery_timeout') {
2530
+ return `${providerLabel} did not finish during the final creation recovery window, so the task was stopped permanently.`;
2531
+ }
2366
2532
  if (code === 'worker_shutdown') {
2367
2533
  return 'The worker stopped while running this task. Start the worker again, then retry the task.';
2368
2534
  }
@@ -2422,6 +2588,24 @@ async function createLocalPlaydropShim(input) {
2422
2588
  }
2423
2589
  return binDir;
2424
2590
  }
2591
+ async function createStaticGameCleanToolPath(workspaceDir, runtime) {
2592
+ const binDir = node_path_1.default.join(workspaceDir, '.clean-tools');
2593
+ await (0, promises_1.rm)(binDir, { recursive: true, force: true });
2594
+ await (0, promises_1.mkdir)(binDir, { recursive: true });
2595
+ const commands = runtime === 'CODEX'
2596
+ ? ['codex', 'node', 'npm', 'npx']
2597
+ : ['claude', 'node', 'npm', 'npx'];
2598
+ for (const command of commands) {
2599
+ const target = command === 'node'
2600
+ ? node_process_1.default.execPath
2601
+ : (await execFileAsync('/usr/bin/which', [command])).stdout.trim();
2602
+ if (!target || !node_path_1.default.isAbsolute(target)) {
2603
+ throw new Error(`static_game_clean_tool_missing:${command}`);
2604
+ }
2605
+ (0, node_fs_1.symlinkSync)(target, node_path_1.default.join(binDir, command));
2606
+ }
2607
+ return binDir;
2608
+ }
2425
2609
  function probeCodexInstallation() {
2426
2610
  const probe = (0, shellProbe_1.runShell)((0, shellProbe_1.buildCommandPathProbe)('codex'));
2427
2611
  if (probe.exitCode !== 0 || !probe.output.trim()) {
@@ -2643,6 +2827,111 @@ function errorMessage(error) {
2643
2827
  function errorCode(error) {
2644
2828
  return errorMessage(error).split(':')[0]?.trim() || 'unknown_error';
2645
2829
  }
2830
+ function workerCommandExists(command) {
2831
+ const probe = (0, shellProbe_1.runShell)((0, shellProbe_1.buildCommandPathProbe)(command));
2832
+ return probe.exitCode === 0 && Boolean(probe.output.trim());
2833
+ }
2834
+ function resolveInstalledChromeExecutable() {
2835
+ if (node_process_1.default.platform === 'darwin') {
2836
+ const executable = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
2837
+ return (0, node_fs_1.existsSync)(executable) ? executable : null;
2838
+ }
2839
+ if (node_process_1.default.platform === 'win32') {
2840
+ const roots = [node_process_1.default.env.PROGRAMFILES, node_process_1.default.env['PROGRAMFILES(X86)'], node_process_1.default.env.LOCALAPPDATA]
2841
+ .map((value) => value?.trim())
2842
+ .filter((value) => Boolean(value));
2843
+ for (const root of roots) {
2844
+ const executable = node_path_1.default.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe');
2845
+ if ((0, node_fs_1.existsSync)(executable)) {
2846
+ return executable;
2847
+ }
2848
+ }
2849
+ return null;
2850
+ }
2851
+ return workerCommandExists('google-chrome') ? 'google-chrome' : null;
2852
+ }
2853
+ function collectWorkerInfrastructureFailures() {
2854
+ const failures = [];
2855
+ if (!resolveInstalledChromeExecutable()) {
2856
+ failures.push('worker_preflight_google_chrome_missing');
2857
+ }
2858
+ try {
2859
+ (0, runtime_1.resolvePlaywrightMcpCliPath)();
2860
+ }
2861
+ catch (error) {
2862
+ failures.push(errorCode(error));
2863
+ }
2864
+ if (node_process_1.default.platform !== 'darwin') {
2865
+ return failures;
2866
+ }
2867
+ if (node_process_1.default.arch !== recorderRelease_1.MAC_WORKER_RECORDER_RELEASE.architecture) {
2868
+ failures.push('worker_preflight_recorder_architecture_unsupported');
2869
+ return failures;
2870
+ }
2871
+ if (!workerCommandExists('ffmpeg')) {
2872
+ failures.push('worker_preflight_ffmpeg_missing');
2873
+ }
2874
+ if (!workerCommandExists('ffprobe')) {
2875
+ failures.push('worker_preflight_ffprobe_missing');
2876
+ }
2877
+ const guiSession = (0, shellProbe_1.runShell)('/bin/launchctl print "gui/$(/usr/bin/id -u)"');
2878
+ if (guiSession.exitCode !== 0) {
2879
+ failures.push('worker_preflight_gui_session_missing');
2880
+ }
2881
+ const recorderPath = (0, recorderRelease_1.resolveMacWorkerRecorderPath)();
2882
+ if (!(0, node_fs_1.existsSync)(recorderPath)) {
2883
+ failures.push('worker_preflight_recorder_missing');
2884
+ return failures;
2885
+ }
2886
+ const recorderStat = (0, node_fs_1.statSync)(recorderPath);
2887
+ if (!recorderStat.isFile() || (recorderStat.mode & 0o111) === 0) {
2888
+ failures.push('worker_preflight_recorder_not_executable');
2889
+ return failures;
2890
+ }
2891
+ const installedSha256 = (0, recorderRelease_1.recorderSha256)((0, node_fs_1.readFileSync)(recorderPath));
2892
+ if (installedSha256 !== recorderRelease_1.MAC_WORKER_RECORDER_RELEASE.sha256) {
2893
+ failures.push('worker_preflight_recorder_hash_mismatch');
2894
+ return failures;
2895
+ }
2896
+ const permission = (0, node_child_process_1.spawnSync)(recorderPath, ['--check-permission'], {
2897
+ encoding: 'utf8',
2898
+ timeout: 10000,
2899
+ });
2900
+ if (permission.status !== 0) {
2901
+ failures.push('worker_preflight_screen_recording_permission_missing');
2902
+ }
2903
+ return failures;
2904
+ }
2905
+ function describeWorkerPreflightReason(reason) {
2906
+ if (reason.startsWith('worker_preflight_recorder_download_failed:')) {
2907
+ return `The PlayDrop macOS recorder could not be downloaded: ${reason.slice('worker_preflight_recorder_download_failed:'.length).trim()}`;
2908
+ }
2909
+ const descriptions = {
2910
+ worker_preflight_google_chrome_missing: 'Google Chrome is missing from its standard installation path.',
2911
+ worker_preflight_playwright_missing: 'The PlayDrop CLI cannot load Playwright.',
2912
+ worker_preflight_chromium_missing: 'The Chromium executable required by Playwright is missing.',
2913
+ worker_preflight_playwright_mcp_missing: 'The pinned Playwright MCP package is missing from the PlayDrop CLI installation.',
2914
+ worker_preflight_playwright_mcp_version: 'The installed Playwright MCP package does not match the PlayDrop CLI release.',
2915
+ worker_preflight_ffmpeg_missing: 'ffmpeg is missing from PATH.',
2916
+ worker_preflight_ffprobe_missing: 'ffprobe is missing from PATH.',
2917
+ worker_preflight_gui_session_missing: 'No active macOS GUI session is available.',
2918
+ worker_preflight_recorder_architecture_unsupported: 'The native recorder is currently available only for Apple silicon Macs.',
2919
+ worker_preflight_recorder_missing: 'The PlayDrop macOS recorder is missing.',
2920
+ worker_preflight_recorder_not_executable: 'The PlayDrop macOS recorder is not executable.',
2921
+ worker_preflight_recorder_hash_mismatch: 'The PlayDrop macOS recorder does not match this CLI release.',
2922
+ worker_preflight_screen_recording_permission_missing: 'Screen Recording permission is not granted to the PlayDrop recorder.',
2923
+ codex_not_authenticated: 'Codex is installed but not authenticated.',
2924
+ codex_models_unavailable: 'Codex is authenticated but reported no usable models.',
2925
+ claude_not_authenticated: 'Claude Code is installed but not authenticated.',
2926
+ claude_models_unavailable: 'Claude Code is authenticated but reported no usable models.',
2927
+ agent_cli_not_found: 'No supported authenticated agent CLI is installed.',
2928
+ };
2929
+ return descriptions[reason] ?? reason;
2930
+ }
2931
+ function formatWorkerPreflightFailure(reasons) {
2932
+ const uniqueReasons = Array.from(new Set(reasons));
2933
+ return new Error(`worker_preflight_failed:\n${uniqueReasons.map((reason) => `- ${describeWorkerPreflightReason(reason)}`).join('\n')}`);
2934
+ }
2646
2935
  function readWorkerTargetUpdateFailure() {
2647
2936
  const failureFile = node_process_1.default.env.PLAYDROP_WORKER_TARGET_UPDATE_FAILURE_FILE?.trim() ?? '';
2648
2937
  if (!failureFile || !(0, node_fs_1.existsSync)(failureFile)) {
@@ -2668,7 +2957,7 @@ function collectWorkerLocalStatus() {
2668
2957
  let claudeVersion = null;
2669
2958
  let claudeModels = [];
2670
2959
  let cursorVersion = null;
2671
- let npmVersion = null;
2960
+ const npmVersion = null;
2672
2961
  let playwright = null;
2673
2962
  let targetUpdateFailure = null;
2674
2963
  try {
@@ -2691,12 +2980,6 @@ function collectWorkerLocalStatus() {
2691
2980
  catch (error) {
2692
2981
  degradedReasons.push(errorCode(error));
2693
2982
  }
2694
- try {
2695
- npmVersion = probeNpmVersion();
2696
- }
2697
- catch (error) {
2698
- degradedReasons.push(errorCode(error));
2699
- }
2700
2983
  try {
2701
2984
  playwright = probePlaywrightChromium();
2702
2985
  if (!playwright.chromiumInstalled) {
@@ -2706,6 +2989,7 @@ function collectWorkerLocalStatus() {
2706
2989
  catch (error) {
2707
2990
  degradedReasons.push(errorCode(error));
2708
2991
  }
2992
+ degradedReasons.push(...collectWorkerInfrastructureFailures());
2709
2993
  try {
2710
2994
  targetUpdateFailure = readWorkerTargetUpdateFailure();
2711
2995
  }
@@ -2739,7 +3023,8 @@ function collectWorkerLocalStatus() {
2739
3023
  degradedReasons.push(errorCode(error));
2740
3024
  }
2741
3025
  const uniqueReasons = Array.from(new Set(degradedReasons));
2742
- const infrastructureReady = Boolean(npmVersion && playwright?.chromiumInstalled);
3026
+ const infrastructureFailures = degradedReasons.filter((reason) => reason.startsWith('worker_preflight_'));
3027
+ const infrastructureReady = Boolean(playwright?.chromiumInstalled) && infrastructureFailures.length === 0;
2743
3028
  const ready = Boolean(capabilities?.ready) && infrastructureReady;
2744
3029
  return {
2745
3030
  ready,
@@ -2759,6 +3044,11 @@ function collectWorkerLocalStatus() {
2759
3044
  function buildWorkerSetupActions(input) {
2760
3045
  const reasons = new Set(input.degradedReasons);
2761
3046
  const actions = [];
3047
+ const addManualAction = (reason, label) => {
3048
+ if (reasons.has(reason)) {
3049
+ actions.push({ reason, label, message: describeWorkerPreflightReason(reason) });
3050
+ }
3051
+ };
2762
3052
  if (reasons.has('worker_preflight_chromium_missing')) {
2763
3053
  const version = input.playwright?.version && input.playwright.version !== 'unknown'
2764
3054
  ? `@${input.playwright.version}`
@@ -2787,6 +3077,17 @@ function buildWorkerSetupActions(input) {
2787
3077
  message: 'Install and authenticate Codex or Claude Code, then rerun playdrop agents status --json.',
2788
3078
  });
2789
3079
  }
3080
+ addManualAction('worker_preflight_google_chrome_missing', 'Install Google Chrome');
3081
+ addManualAction('worker_preflight_playwright_mcp_missing', 'Repair the PlayDrop CLI installation');
3082
+ addManualAction('worker_preflight_playwright_mcp_version', 'Repair the PlayDrop CLI installation');
3083
+ addManualAction('worker_preflight_ffmpeg_missing', 'Install ffmpeg');
3084
+ addManualAction('worker_preflight_ffprobe_missing', 'Install ffprobe');
3085
+ addManualAction('worker_preflight_gui_session_missing', 'Sign in to the Mac desktop');
3086
+ addManualAction('worker_preflight_recorder_architecture_unsupported', 'Use an Apple silicon Mac');
3087
+ addManualAction('worker_preflight_recorder_missing', 'Restart the worker to download the recorder');
3088
+ addManualAction('worker_preflight_recorder_not_executable', 'Restart the worker to repair the recorder');
3089
+ addManualAction('worker_preflight_recorder_hash_mismatch', 'Restart the worker to repair the recorder');
3090
+ addManualAction('worker_preflight_screen_recording_permission_missing', 'Grant Screen Recording permission');
2790
3091
  return actions;
2791
3092
  }
2792
3093
  function resolveWorkerStateDir() {
@@ -3609,7 +3910,7 @@ async function setupWorker(options = {}) {
3609
3910
  const workerName = explicitWorkerName || `${username} - ${node_os_1.default.hostname()}`;
3610
3911
  const local = collectWorkerLocalStatus();
3611
3912
  const agentReady = local.agents.some((agent) => agent.ready === true);
3612
- const baseRuntimeReady = Boolean(agentReady && local.npmVersion && local.playwright?.chromiumInstalled);
3913
+ const baseRuntimeReady = Boolean(agentReady && local.playwright?.chromiumInstalled);
3613
3914
  if (!baseRuntimeReady) {
3614
3915
  throw new Error(`worker_setup_preflight_failed: ${local.degradedReasons.join(', ') || 'local worker runtime is not ready'}`);
3615
3916
  }
@@ -3799,7 +4100,7 @@ on run matchers
3799
4100
  return "remaining=" & remainingCount & ";blank=" & blankWindowCount
3800
4101
  end run
3801
4102
  `;
3802
- async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
4103
+ async function enforceInstrumentBrowserHygieneUnlocked(assignment, options = {}) {
3803
4104
  if (assignment.agent.browser === 'NONE') {
3804
4105
  return { remainingTaskTabs: 0, blankWindows: 0 };
3805
4106
  }
@@ -3814,13 +4115,22 @@ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3814
4115
  const result = await execFileAsync(command, args, { timeout: 15000, maxBuffer: 1024 * 1024 });
3815
4116
  return { stdout: result.stdout, stderr: result.stderr };
3816
4117
  });
3817
- let result;
3818
- try {
3819
- result = await runner('osascript', ['-e', INSTRUMENT_BROWSER_HYGIENE_APPLESCRIPT, ...matchers]);
3820
- }
3821
- catch (error) {
3822
- throw new Error(`worker_browser_hygiene_automation_failed:${error instanceof Error ? error.message : String(error)}`);
4118
+ let result = null;
4119
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
4120
+ try {
4121
+ result = await runner('osascript', ['-e', INSTRUMENT_BROWSER_HYGIENE_APPLESCRIPT, ...matchers]);
4122
+ break;
4123
+ }
4124
+ catch (error) {
4125
+ const message = error instanceof Error ? error.message : String(error);
4126
+ if (!message.includes('Connection is invalid. (-609)') || attempt === 3) {
4127
+ throw new Error(`worker_browser_hygiene_automation_failed:${message}`);
4128
+ }
4129
+ await sleep(250);
4130
+ }
3823
4131
  }
4132
+ if (!result)
4133
+ throw new Error('worker_browser_hygiene_automation_failed:no_result');
3824
4134
  const match = /remaining=(\d+);blank=(\d+)/.exec(result.stdout.trim());
3825
4135
  if (!match) {
3826
4136
  throw new Error(`worker_browser_hygiene_invalid_result:${result.stdout.trim() || result.stderr.trim() || 'empty'}`);
@@ -3832,12 +4142,25 @@ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3832
4142
  }
3833
4143
  return { remainingTaskTabs, blankWindows };
3834
4144
  }
4145
+ let instrumentBrowserHygieneTail = Promise.resolve();
4146
+ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
4147
+ const result = instrumentBrowserHygieneTail.then(() => enforceInstrumentBrowserHygieneUnlocked(assignment, options));
4148
+ instrumentBrowserHygieneTail = result.then(() => undefined, () => undefined);
4149
+ return result;
4150
+ }
4151
+ function assignmentRequiresRegisteredBundle(assignment) {
4152
+ const playdropMetadata = assignment.task.metadata.playdrop;
4153
+ return Boolean(playdropMetadata
4154
+ && typeof playdropMetadata === 'object'
4155
+ && !Array.isArray(playdropMetadata)
4156
+ && playdropMetadata.bundleSource === 'REGISTERED');
4157
+ }
3835
4158
  function throwWorkerRuntimeError(error) {
3836
4159
  throw error;
3837
4160
  }
3838
4161
  async function startWorker(options = {}) {
3839
4162
  loadWorkerEnvFile(options.env);
3840
- await (0, commandContext_1.withEnvironment)('worker start', 'Starting the PlayDrop worker', async ({ client, env }) => {
4163
+ await (0, commandContext_1.withEnvironment)('worker start', 'Starting the PlayDrop worker', async ({ client, env, envConfig, token }) => {
3841
4164
  const devPluginWorkingTree = resolveDevPluginWorkingTree({
3842
4165
  workingTree: options.devPluginWorkingTree,
3843
4166
  env,
@@ -3870,15 +4193,35 @@ async function startWorker(options = {}) {
3870
4193
  const supervisorLock = acquireWorkerSupervisorLock({ supervisorType, workerName });
3871
4194
  try {
3872
4195
  await removeStaleAgentRunCredentials();
3873
- const npmVersion = probeNpmVersion();
3874
- const playwright = probePlaywrightChromium();
4196
+ const startupFailures = [];
4197
+ if (node_process_1.default.platform === 'darwin') {
4198
+ try {
4199
+ const recorder = await (0, recorderRelease_1.ensureMacWorkerRecorderInstalled)();
4200
+ if (recorder.downloaded) {
4201
+ console.log(`Installed PlayDrop macOS recorder ${recorderRelease_1.MAC_WORKER_RECORDER_RELEASE.version} at ${recorder.path}.`);
4202
+ }
4203
+ }
4204
+ catch (error) {
4205
+ startupFailures.push(`worker_preflight_recorder_download_failed:${errorMessage(error)}`);
4206
+ }
4207
+ }
4208
+ else if (node_process_1.default.platform === 'win32') {
4209
+ console.warn('Native listing recording is not available on Windows yet.');
4210
+ }
4211
+ const local = collectWorkerLocalStatus();
4212
+ startupFailures.push(...local.degradedReasons);
4213
+ if (startupFailures.length > 0 || !local.ready || !local.capabilities || !local.playwright?.chromiumInstalled) {
4214
+ throw formatWorkerPreflightFailure(startupFailures);
4215
+ }
4216
+ const npmVersion = local.npmVersion;
4217
+ const playwright = {
4218
+ version: local.playwright.version ?? 'unknown',
4219
+ chromiumInstalled: true,
4220
+ ...(local.playwright.executablePath ? { executablePath: local.playwright.executablePath } : {}),
4221
+ };
3875
4222
  const workerKey = (0, config_1.getOrCreateWorkerKey)();
3876
4223
  const maxParallelTasks = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS);
3877
- let capabilities = probeReadyWorkerCapabilities({
3878
- npmVersion,
3879
- playwright,
3880
- maxParallelTasks,
3881
- });
4224
+ let capabilities = local.capabilities;
3882
4225
  let lastCapabilityRefreshAt = Date.now();
3883
4226
  writeWorkerRuntimeState({
3884
4227
  env,
@@ -4033,6 +4376,7 @@ async function startWorker(options = {}) {
4033
4376
  let heartbeatObservedFinalizedTask = false;
4034
4377
  let retainWorkspace = false;
4035
4378
  let workspaceDir = null;
4379
+ let agentWorkspaceDir = null;
4036
4380
  let availableSkillPaths = [];
4037
4381
  const observedSkillPaths = new Set();
4038
4382
  const assignment = claimedAssignment;
@@ -4062,6 +4406,38 @@ async function startWorker(options = {}) {
4062
4406
  });
4063
4407
  telemetryReported = true;
4064
4408
  };
4409
+ const failUploadedArtifactWithoutTaskDone = async () => {
4410
+ const failureCode = 'agent_exited_after_upload_without_task_done';
4411
+ retainWorkspace = true;
4412
+ await client.workerCreateAgentTaskEvent(task.id, {
4413
+ workerKey,
4414
+ leaseToken,
4415
+ kind: 'progress',
4416
+ phase: 'failed',
4417
+ message: 'The agent uploaded an artifact but exited without marking the task done.',
4418
+ pct: null,
4419
+ payload: { error: failureCode },
4420
+ });
4421
+ await failTaskWithRetry(client, task.id, {
4422
+ workerKey,
4423
+ leaseToken,
4424
+ error: failureCode,
4425
+ terminalReason: {
4426
+ kind: 'failure',
4427
+ category: 'agent_failure',
4428
+ code: failureCode,
4429
+ message: 'The agent uploaded an artifact but did not complete the task protocol.',
4430
+ retryable: false,
4431
+ rawError: failureCode,
4432
+ source: 'agent',
4433
+ },
4434
+ result: {
4435
+ stage: 'completion',
4436
+ uploadedArtifactPreserved: true,
4437
+ },
4438
+ });
4439
+ await reportTelemetry('FAILED');
4440
+ };
4065
4441
  try {
4066
4442
  const codexModel = assignment.agent.runtime === 'CODEX'
4067
4443
  ? resolveCodexModel(assignment.agent.model, assignment.agent.reasoningEffort)
@@ -4071,24 +4447,35 @@ async function startWorker(options = {}) {
4071
4447
  : null;
4072
4448
  resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.agent.model;
4073
4449
  reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
4450
+ const cleanRoom = task.kind === 'STATIC_GAME';
4074
4451
  workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
4075
- await pruneWorkerTaskWorkspaces(workspaceDir);
4076
- await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
4077
- await (0, promises_1.mkdir)(workspaceDir, { recursive: true });
4078
- const eventDir = node_path_1.default.join(workspaceDir, '.playdrop', 'task-events');
4079
- await (0, promises_1.mkdir)(eventDir, { recursive: true });
4080
- await stageWorkerTaskContext({
4081
- workspaceDir,
4082
- env,
4083
- devPort,
4084
- taskContext,
4085
- });
4086
- const binDir = await createLocalPlaydropShim({
4452
+ const preparedWorkspace = await prepareClaimedTaskWorkspace({
4087
4453
  workspaceDir,
4088
- taskId: taskContext.taskId,
4089
- attempt: taskContext.attempt,
4090
- devPort,
4454
+ attempt: task.attempts,
4091
4455
  });
4456
+ if (preparedWorkspace.resumed) {
4457
+ console.log(`Agent task ${task.id} retry resumed retained workspace attempt ${task.attempts}.`);
4458
+ }
4459
+ agentWorkspaceDir = cleanRoom ? node_path_1.default.join(workspaceDir, 'project') : workspaceDir;
4460
+ await (0, promises_1.mkdir)(agentWorkspaceDir, { recursive: true });
4461
+ const eventDir = node_path_1.default.join(workspaceDir, '.playdrop', 'task-events');
4462
+ await (0, promises_1.mkdir)(eventDir, { recursive: true });
4463
+ if (!cleanRoom) {
4464
+ await stageWorkerTaskContext({
4465
+ workspaceDir,
4466
+ env,
4467
+ devPort,
4468
+ taskContext,
4469
+ });
4470
+ }
4471
+ const binDir = cleanRoom
4472
+ ? await createStaticGameCleanToolPath(workspaceDir, assignment.agent.runtime)
4473
+ : await createLocalPlaydropShim({
4474
+ workspaceDir,
4475
+ taskId: taskContext.taskId,
4476
+ attempt: taskContext.attempt,
4477
+ devPort,
4478
+ });
4092
4479
  const handleEventDrainFailure = (error) => {
4093
4480
  const classification = classifyWorkerEventDrainError(error);
4094
4481
  if (classification === 'session_expired') {
@@ -4120,17 +4507,59 @@ async function startWorker(options = {}) {
4120
4507
  appendObservedSkillPathsFromTranscript(chunks);
4121
4508
  await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
4122
4509
  };
4123
- await stageAssignmentWorkspaceV2(workspaceDir, assignment.workspace);
4124
- const assignmentPluginRoot = devPluginWorkingTree
4125
- ? await stageAgentBundleWorkspaceFiles(devPluginWorkingTree, workspaceDir, task.kind)
4510
+ await stageAssignmentWorkspaceV2(agentWorkspaceDir, assignment.workspace);
4511
+ const assignmentPluginRoot = !cleanRoom
4512
+ && devPluginWorkingTree
4513
+ && !assignmentRequiresRegisteredBundle(assignment)
4514
+ ? await stageDevPluginWorkingTree({
4515
+ workingTree: devPluginWorkingTree,
4516
+ workspaceDir: agentWorkspaceDir,
4517
+ taskKind: task.kind,
4518
+ })
4126
4519
  : assignment.plugin
4127
4520
  ? await stageAssignmentPluginBundle({
4128
- workspaceDir,
4521
+ workspaceDir: agentWorkspaceDir,
4522
+ extractionDir: workspaceDir,
4129
4523
  pluginBundle: assignment.plugin,
4130
4524
  taskKind: task.kind,
4131
4525
  })
4132
4526
  : null;
4133
4527
  availableSkillPaths = listPluginSkillPaths(assignmentPluginRoot);
4528
+ if (cleanRoom) {
4529
+ const playdropMetadata = assignment.task.metadata.playdrop;
4530
+ const postAgentAction = playdropMetadata && typeof playdropMetadata === 'object' && !Array.isArray(playdropMetadata)
4531
+ ? playdropMetadata.postAgentAction
4532
+ : null;
4533
+ const publishInstruction = postAgentAction && typeof postAgentAction === 'object' && !Array.isArray(postAgentAction)
4534
+ ? postAgentAction
4535
+ : null;
4536
+ if (!assignment.plugin
4537
+ || assignmentPluginRoot !== null
4538
+ || assignment.agent.browser !== 'NONE'
4539
+ || publishInstruction?.type !== 'PUBLISH_STATIC_GAME'
4540
+ || publishInstruction.entryPoint !== 'index.html') {
4541
+ throw new Error('static_game_clean_room_contract_invalid');
4542
+ }
4543
+ availableSkillPaths = [];
4544
+ await client.workerCreateAgentTaskEvent(task.id, {
4545
+ workerKey,
4546
+ leaseToken,
4547
+ kind: 'system',
4548
+ phase: 'clean_room',
4549
+ message: 'Static game clean-room capability evidence recorded.',
4550
+ payload: {
4551
+ bundleHash: assignment.plugin.sha256,
4552
+ requestedModel: assignment.agent.model,
4553
+ resolvedModel: resolvedRuntimeModel,
4554
+ networkAccess: true,
4555
+ browser: assignment.agent.browser,
4556
+ availableSkills: [],
4557
+ pluginInstalled: false,
4558
+ playdropCliInPath: false,
4559
+ taskCredentialsExposed: false,
4560
+ },
4561
+ });
4562
+ }
4134
4563
  heartbeatTimer = setInterval(() => {
4135
4564
  client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
4136
4565
  .then((heartbeat) => {
@@ -4189,81 +4618,161 @@ async function startWorker(options = {}) {
4189
4618
  handleEventDrainFailure(error);
4190
4619
  });
4191
4620
  }, 1000);
4192
- const prompt = assignment.request.prompt;
4621
+ const gameCreationTask = usesCreatorRecovery(task.kind);
4622
+ const startsInCreationRecovery = gameCreationTask && preparedWorkspace.resumed;
4623
+ const prompt = startsInCreationRecovery
4624
+ ? buildCreatorRecoveryPrompt({
4625
+ prompt: assignment.request.prompt,
4626
+ kind: task.kind,
4627
+ })
4628
+ : task.kind === 'GAME_EVAL' && preparedWorkspace.resumed
4629
+ ? buildGameEvalRetryPrompt(assignment.request.prompt)
4630
+ : assignment.request.prompt;
4193
4631
  const imageAttachments = assignment.request.attachments
4194
4632
  .filter((attachment) => attachment.contentType.startsWith('image/'))
4195
4633
  .map((attachment) => ({
4196
- path: resolveWorkspaceFileDestination(workspaceDir, attachment.path),
4634
+ path: resolveWorkspaceFileDestination(agentWorkspaceDir, attachment.path),
4197
4635
  contentType: attachment.contentType,
4198
4636
  }));
4199
4637
  if (shuttingDown) {
4200
4638
  throw new Error('worker_shutdown');
4201
4639
  }
4640
+ const runAssignedAgent = async (input) => {
4641
+ if (assignment.agent.runtime === 'CODEX') {
4642
+ return runCodex({
4643
+ workspaceDir: agentWorkspaceDir,
4644
+ stateDir: cleanRoom ? workspaceDir : undefined,
4645
+ binDir,
4646
+ eventDir,
4647
+ prompt: input.prompt,
4648
+ envName: env,
4649
+ workerUsername: username,
4650
+ taskId: task.id,
4651
+ taskToken: assignment.task.token,
4652
+ attempt: taskContext.attempt,
4653
+ devPort,
4654
+ codexModel: codexModel ?? resolveCodexModel(assignment.agent.model),
4655
+ taskPluginRoot: assignmentPluginRoot,
4656
+ enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4657
+ instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4658
+ imagePaths: imageAttachments.map((attachment) => attachment.path),
4659
+ onTranscriptChunks: async (chunks) => {
4660
+ await appendTranscriptChunks(chunks);
4661
+ },
4662
+ onChild: (controls) => {
4663
+ activeTerminators.set(task.id, controls.terminate);
4664
+ },
4665
+ cleanRoom,
4666
+ timeoutMs: input.timeoutMs,
4667
+ resumeSessionId: input.resumeSessionId,
4668
+ });
4669
+ }
4670
+ if (assignment.agent.runtime === 'CLAUDE_CODE') {
4671
+ return runClaude({
4672
+ workspaceDir: agentWorkspaceDir,
4673
+ stateDir: cleanRoom ? workspaceDir : undefined,
4674
+ binDir,
4675
+ eventDir,
4676
+ prompt: input.prompt,
4677
+ enableChrome: assignment.agent.browser === 'CHROME',
4678
+ enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4679
+ instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4680
+ envName: env,
4681
+ workerUsername: username,
4682
+ taskId: task.id,
4683
+ taskToken: assignment.task.token,
4684
+ attempt: taskContext.attempt,
4685
+ devPort,
4686
+ claudeModel: claudeModel ?? resolveClaudeModel(assignment.agent.model),
4687
+ taskPluginRoot: assignmentPluginRoot,
4688
+ imageAttachments,
4689
+ onTranscriptChunks: async (chunks) => {
4690
+ await appendTranscriptChunks(chunks);
4691
+ },
4692
+ onChild: (controls) => {
4693
+ activeTerminators.set(task.id, controls.terminate);
4694
+ },
4695
+ cleanRoom,
4696
+ timeoutMs: input.timeoutMs,
4697
+ resumeSessionId: input.resumeSessionId,
4698
+ });
4699
+ }
4700
+ if (assignment.agent.runtime === 'CURSOR_COMPOSER') {
4701
+ await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4702
+ workerKey,
4703
+ leaseToken,
4704
+ attemptIndex: 0,
4705
+ reason: 'unavailable_runtime',
4706
+ message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4707
+ });
4708
+ fenced = true;
4709
+ throw new Error('worker_cursor_composer_runner_not_configured');
4710
+ }
4711
+ throw new Error(`unsupported_worker_agent:${assignment.agent.runtime}`);
4712
+ };
4202
4713
  agentStartedAt = new Date();
4203
- if (assignment.agent.runtime === 'CODEX') {
4204
- agentResult = await runCodex({
4205
- workspaceDir,
4206
- binDir,
4207
- eventDir,
4208
- prompt,
4209
- envName: env,
4210
- workerUsername: username,
4211
- taskId: task.id,
4212
- taskToken: assignment.task.token,
4213
- attempt: taskContext.attempt,
4214
- devPort,
4215
- codexModel: codexModel ?? resolveCodexModel(assignment.agent.model),
4216
- taskPluginRoot: assignmentPluginRoot,
4217
- enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4218
- instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4219
- imagePaths: imageAttachments.map((attachment) => attachment.path),
4220
- onTranscriptChunks: async (chunks) => {
4221
- await appendTranscriptChunks(chunks);
4222
- },
4223
- onChild: (controls) => {
4224
- activeTerminators.set(task.id, controls.terminate);
4225
- },
4226
- });
4227
- }
4228
- else if (assignment.agent.runtime === 'CLAUDE_CODE') {
4229
- agentResult = await runClaude({
4230
- workspaceDir,
4231
- binDir,
4232
- eventDir,
4233
- prompt,
4234
- enableChrome: assignment.agent.browser === 'CHROME',
4235
- enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4236
- instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4237
- envName: env,
4238
- workerUsername: username,
4239
- taskId: task.id,
4240
- taskToken: assignment.task.token,
4241
- attempt: taskContext.attempt,
4242
- devPort,
4243
- claudeModel: claudeModel ?? resolveClaudeModel(assignment.agent.model),
4244
- taskPluginRoot: assignmentPluginRoot,
4245
- imageAttachments,
4246
- onTranscriptChunks: async (chunks) => {
4247
- await appendTranscriptChunks(chunks);
4248
- },
4249
- onChild: (controls) => {
4250
- activeTerminators.set(task.id, controls.terminate);
4251
- },
4252
- });
4253
- }
4254
- else if (assignment.agent.runtime === 'CURSOR_COMPOSER') {
4255
- await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4714
+ if (startsInCreationRecovery) {
4715
+ await client.workerCreateAgentTaskEvent(task.id, {
4256
4716
  workerKey,
4257
4717
  leaseToken,
4258
- attemptIndex: 0,
4259
- reason: 'unavailable_runtime',
4260
- message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4718
+ kind: 'progress',
4719
+ phase: 'creation_recovery',
4720
+ message: 'Resumed the retained game workspace with a focused creation recovery window.',
4261
4721
  });
4262
- fenced = true;
4263
- throw new Error('worker_cursor_composer_runner_not_configured');
4264
4722
  }
4265
- else {
4266
- throw new Error(`unsupported_worker_agent:${assignment.agent.runtime}`);
4723
+ agentResult = await runAssignedAgent({
4724
+ prompt,
4725
+ timeoutMs: startsInCreationRecovery
4726
+ ? runtime_1.DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS
4727
+ : gameCreationTask
4728
+ ? runtime_1.DEFAULT_CREATOR_AGENT_TIMEOUT_MS
4729
+ : undefined,
4730
+ });
4731
+ if (startsInCreationRecovery && agentResult.timedOut) {
4732
+ const marker = 'agent_task_creation_recovery_timeout';
4733
+ agentResult = {
4734
+ ...agentResult,
4735
+ stderr: `${agentResult.stderr}\n${marker}`.trim(),
4736
+ outputTail: `${agentResult.outputTail}\n${marker}`.trim(),
4737
+ };
4738
+ }
4739
+ if (gameCreationTask
4740
+ && !startsInCreationRecovery
4741
+ && agentResult.timedOut
4742
+ && !fenced
4743
+ && !shuttingDown) {
4744
+ const timedOutResult = agentResult;
4745
+ const refreshedAfterTimeout = await fetchTaskDetail(client, target, task.id);
4746
+ if (refreshedAfterTimeout.task.status === 'RUNNING'
4747
+ && !hasAgentTaskUploadedArtifact(refreshedAfterTimeout.task)) {
4748
+ const providerSessionId = timedOutResult.providerSessionId?.trim() ?? '';
4749
+ if (!providerSessionId) {
4750
+ const marker = 'agent_task_resume_session_missing';
4751
+ agentResult = {
4752
+ ...timedOutResult,
4753
+ stderr: `${timedOutResult.stderr}\n${marker}`.trim(),
4754
+ outputTail: `${timedOutResult.outputTail}\n${marker}`.trim(),
4755
+ };
4756
+ }
4757
+ else {
4758
+ await client.workerCreateAgentTaskEvent(task.id, {
4759
+ workerKey,
4760
+ leaseToken,
4761
+ kind: 'progress',
4762
+ phase: 'creation_recovery',
4763
+ message: 'The one-hour creation ceiling was reached. Resuming the same agent conversation to finish the existing game.',
4764
+ });
4765
+ const recoveryResult = await runAssignedAgent({
4766
+ prompt: buildCreatorRecoveryPrompt({
4767
+ prompt: assignment.request.prompt,
4768
+ kind: task.kind,
4769
+ }),
4770
+ timeoutMs: runtime_1.DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS,
4771
+ resumeSessionId: providerSessionId,
4772
+ });
4773
+ agentResult = mergeCreatorRecoveryResults(assignment.agent.runtime, timedOutResult, recoveryResult);
4774
+ }
4775
+ }
4267
4776
  }
4268
4777
  agentCompletedAt = new Date();
4269
4778
  await queueEventDrain().catch((error) => {
@@ -4309,16 +4818,8 @@ async function startWorker(options = {}) {
4309
4818
  await reportTelemetry(refreshed.task.status);
4310
4819
  }
4311
4820
  else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
4312
- retainWorkspace = true;
4313
- console.error(`Agent task ${task.id} already uploaded an artifact but exited before task completion. Leaving it for manual completion repair.`);
4314
- await client.workerCreateAgentTaskEvent(task.id, {
4315
- workerKey,
4316
- leaseToken,
4317
- kind: 'progress',
4318
- phase: 'upload',
4319
- message: 'Agent exited after upload before task done; manual completion repair required.',
4320
- });
4321
- await reportTelemetry('RUNNING');
4821
+ console.error(`Agent task ${task.id} uploaded an artifact but exited before task completion; failing it explicitly.`);
4822
+ await failUploadedArtifactWithoutTaskDone();
4322
4823
  }
4323
4824
  else {
4324
4825
  retainWorkspace = true;
@@ -4352,17 +4853,158 @@ async function startWorker(options = {}) {
4352
4853
  console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
4353
4854
  await reportTelemetry(refreshed.task.status);
4354
4855
  }
4355
- else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
4356
- retainWorkspace = true;
4357
- console.error(`Agent task ${task.id} already uploaded an artifact but exited before task completion. Leaving it for manual completion repair.`);
4358
- await client.workerCreateAgentTaskEvent(task.id, {
4359
- workerKey,
4360
- leaseToken,
4361
- kind: 'progress',
4362
- phase: 'upload',
4363
- message: 'Agent exited after upload before task done; manual completion repair required.',
4856
+ else if (task.kind === 'STATIC_GAME') {
4857
+ const tokenCap = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_TOKEN_CAP', runtime_1.DEFAULT_WORKER_TOKEN_CAP);
4858
+ (0, runtime_1.assertWorkerTokenUsageWithinCap)({
4859
+ tokensUsed: agentResult.tokensUsed,
4860
+ tokenCap,
4364
4861
  });
4365
- await reportTelemetry('RUNNING');
4862
+ await reportTelemetry('DONE');
4863
+ try {
4864
+ let appId = refreshed.task.appId;
4865
+ let appVersionId = refreshed.task.appVersionId;
4866
+ let result;
4867
+ if (Number.isInteger(appId) && Number(appId) > 0
4868
+ && Number.isInteger(appVersionId) && Number(appVersionId) > 0) {
4869
+ result = {
4870
+ appId,
4871
+ appVersionId,
4872
+ appName: refreshed.task.claimedAppName,
4873
+ version: refreshed.task.outputVersion,
4874
+ completedBy: 'worker_static_game_retry',
4875
+ };
4876
+ await client.workerCreateAgentTaskEvent(task.id, {
4877
+ workerKey,
4878
+ leaseToken,
4879
+ kind: 'system',
4880
+ phase: 'static_game_publish_retry',
4881
+ message: 'Reused the static game version already linked to this task.',
4882
+ });
4883
+ }
4884
+ else {
4885
+ const primarySurface = resolveWorkerInstrumentSurface(taskContext);
4886
+ if (!primarySurface) {
4887
+ throw new Error('static_game_artifact_primary_surface_missing');
4888
+ }
4889
+ if (!agentWorkspaceDir) {
4890
+ throw new Error('static_game_artifact_workspace_missing');
4891
+ }
4892
+ await client.workerCreateAgentTaskEvent(task.id, {
4893
+ workerKey,
4894
+ leaseToken,
4895
+ kind: 'system',
4896
+ phase: 'static_game_publish_started',
4897
+ message: 'Publishing the clean-room static game output.',
4898
+ });
4899
+ const taskClient = (0, apiClient_1.createCliApiClient)({
4900
+ baseUrl: envConfig.apiBase,
4901
+ token,
4902
+ onBehalfCreatorUsername: taskContext.creatorUsername,
4903
+ agentTaskToken: assignment.task.token,
4904
+ agentTaskId: task.id,
4905
+ agentTaskAttempt: taskContext.attempt,
4906
+ });
4907
+ const published = await (0, upload_1.publishStaticHtmlProject)({
4908
+ client: taskClient,
4909
+ taskId: task.id,
4910
+ taskToken: assignment.task.token,
4911
+ projectDir: agentWorkspaceDir,
4912
+ creatorUsername: taskContext.creatorUsername,
4913
+ primarySurface,
4914
+ apiBase: envConfig.apiBase,
4915
+ webBase: envConfig.webBase ?? null,
4916
+ claimedAppName: refreshed.task.claimedAppName,
4917
+ devRouterPort: devPort,
4918
+ token,
4919
+ user: me.user,
4920
+ });
4921
+ appId = published.appId;
4922
+ appVersionId = published.appVersionId;
4923
+ result = {
4924
+ appId,
4925
+ appVersionId,
4926
+ appName: published.appName,
4927
+ version: published.version,
4928
+ versionNodeId: published.versionNodeId,
4929
+ creatorUsername: published.creatorUsername,
4930
+ sourceHash: published.sourceHash,
4931
+ runtimeHash: published.runtimeHash,
4932
+ playUrl: published.playUrl,
4933
+ sourceContainsInjectedSdk: published.sourceContainsInjectedSdk,
4934
+ runtimeContainsInjectedSdk: published.runtimeContainsInjectedSdk,
4935
+ hostedLoadCheckPassed: published.hostedLoadCheckPassed,
4936
+ versionVisibility: 'PRIVATE',
4937
+ warnings: published.warnings,
4938
+ completedBy: 'worker_static_game_publish',
4939
+ };
4940
+ }
4941
+ if (!Number.isInteger(appId) || Number(appId) <= 0 || !Number.isInteger(appVersionId) || Number(appVersionId) <= 0) {
4942
+ throw new Error('static_game_publish_linked_version_missing');
4943
+ }
4944
+ await client.workerCompleteAgentTask(task.id, {
4945
+ workerKey,
4946
+ leaseToken,
4947
+ appId: Number(appId),
4948
+ appVersionId: Number(appVersionId),
4949
+ result,
4950
+ summary: 'Created and privately hosted a standalone static web game.',
4951
+ tokensUsed: agentResult.tokensUsed,
4952
+ });
4953
+ retainWorkspace = false;
4954
+ console.log(`Agent task ${task.id} published and completed as STATIC_GAME.`);
4955
+ }
4956
+ catch (error) {
4957
+ const rawMessage = error instanceof Error ? error.message : String(error);
4958
+ const artifactFailure = rawMessage.startsWith('static_game_artifact_');
4959
+ const failureCode = artifactFailure
4960
+ ? rawMessage.split(':', 1)[0]
4961
+ : 'static_game_publish_failed';
4962
+ await client.workerCreateAgentTaskEvent(task.id, {
4963
+ workerKey,
4964
+ leaseToken,
4965
+ kind: 'system',
4966
+ phase: artifactFailure ? 'static_game_artifact_failed' : 'static_game_publish_failed',
4967
+ message: artifactFailure
4968
+ ? `Static game output was invalid: ${rawMessage}`
4969
+ : `Static game publishing failed: ${rawMessage}`,
4970
+ payload: { error: failureCode, detail: rawMessage },
4971
+ });
4972
+ retainWorkspace = true;
4973
+ const terminalReason = artifactFailure
4974
+ ? {
4975
+ kind: 'failure',
4976
+ category: 'upload_validation',
4977
+ code: failureCode,
4978
+ message: 'The static game output did not pass validation.',
4979
+ retryable: false,
4980
+ rawError: rawMessage,
4981
+ source: 'worker',
4982
+ }
4983
+ : {
4984
+ kind: 'failure',
4985
+ category: 'instrument_error',
4986
+ code: failureCode,
4987
+ message: 'The platform failed to publish the static game.',
4988
+ retryable: true,
4989
+ rawError: rawMessage,
4990
+ source: 'worker',
4991
+ };
4992
+ await failTaskWithRetry(client, task.id, {
4993
+ workerKey,
4994
+ leaseToken,
4995
+ error: failureCode,
4996
+ terminalReason,
4997
+ result: {
4998
+ stage: artifactFailure ? 'artifact' : 'publish',
4999
+ error: rawMessage,
5000
+ agentRunSucceeded: true,
5001
+ },
5002
+ });
5003
+ }
5004
+ }
5005
+ else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
5006
+ console.error(`Agent task ${task.id} uploaded an artifact but exited before task completion; failing it explicitly.`);
5007
+ await failUploadedArtifactWithoutTaskDone();
4366
5008
  }
4367
5009
  else {
4368
5010
  const tokenCap = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_TOKEN_CAP', runtime_1.DEFAULT_WORKER_TOKEN_CAP);
@@ -4716,6 +5358,9 @@ async function resolveTaskCommandContext(command, optionsEnv, taskContext) {
4716
5358
  }
4717
5359
  async function uploadTask(options = {}) {
4718
5360
  const taskContext = readTaskContextFile();
5361
+ if (taskContext.kind === 'STATIC_GAME') {
5362
+ throw new Error('task_upload_not_allowed_for_static_game');
5363
+ }
4719
5364
  if (taskContext.kind === 'GAME_REVIEW' || taskContext.kind === 'GAME_EVAL') {
4720
5365
  throw new Error('task_upload_not_allowed_for_game_review');
4721
5366
  }
@@ -4828,6 +5473,7 @@ async function completeTask(options) {
4828
5473
  if (nextSteps === NEXT_STEPS_READ_FAILED) {
4829
5474
  return;
4830
5475
  }
5476
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
4831
5477
  try {
4832
5478
  await ctx.client.workerCompleteAgentTask(taskContext.taskId, {
4833
5479
  taskToken: taskContext.taskToken,
@@ -4844,6 +5490,7 @@ async function completeTask(options) {
4844
5490
  },
4845
5491
  summary,
4846
5492
  ...(nextSteps !== undefined ? { nextSteps } : {}),
5493
+ ...(platformFeedback ? { platformFeedback } : {}),
4847
5494
  });
4848
5495
  }
4849
5496
  catch (error) {
@@ -4858,6 +5505,7 @@ async function completeTask(options) {
4858
5505
  appName: uploadResult.appName,
4859
5506
  version: uploadResult.version,
4860
5507
  },
5508
+ ...(platformFeedback ? { platformFeedback } : {}),
4861
5509
  });
4862
5510
  }
4863
5511
  throw error;
@@ -4903,6 +5551,27 @@ function readOptionalTextFile(filePath) {
4903
5551
  const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
4904
5552
  return content || null;
4905
5553
  }
5554
+ function readPlatformFeedbackFile(filePath) {
5555
+ const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
5556
+ if (!normalizedPath) {
5557
+ return null;
5558
+ }
5559
+ try {
5560
+ const resolved = node_path_1.default.resolve(normalizedPath);
5561
+ if (!(0, node_fs_1.existsSync)(resolved)) {
5562
+ throw new Error('file_missing');
5563
+ }
5564
+ const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
5565
+ if (!content || content.length > 10000) {
5566
+ throw new Error(content ? 'file_too_long' : 'file_empty');
5567
+ }
5568
+ return content;
5569
+ }
5570
+ catch (error) {
5571
+ console.warn(`Platform feedback was not attached: ${error instanceof Error ? error.message : String(error)}`);
5572
+ return null;
5573
+ }
5574
+ }
4906
5575
  function contentTypeForEvidenceFile(fileName) {
4907
5576
  const extension = node_path_1.default.extname(fileName).toLowerCase();
4908
5577
  if (extension === '.png')
@@ -4981,6 +5650,7 @@ async function submitReviewTask(options) {
4981
5650
  const creatorFeedback = state === 'ERROR'
4982
5651
  ? null
4983
5652
  : readOptionalTextFile(creatorFeedbackFile ?? undefined);
5653
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
4984
5654
  await (0, review_1.validateGameReviewResult)({
4985
5655
  reviewState: state,
4986
5656
  reviewMessage,
@@ -5009,6 +5679,7 @@ async function submitReviewTask(options) {
5009
5679
  reviewState: state,
5010
5680
  reviewMessage,
5011
5681
  creatorFeedback,
5682
+ ...(platformFeedback ? { platformFeedback } : {}),
5012
5683
  ...(nextSteps !== undefined ? { nextSteps } : {}),
5013
5684
  evidenceFiles,
5014
5685
  instrumentEvidence,
@@ -5136,11 +5807,13 @@ async function failTask(options) {
5136
5807
  if (!ctx) {
5137
5808
  return;
5138
5809
  }
5810
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
5139
5811
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
5140
5812
  taskToken: taskContext.taskToken,
5141
5813
  error: message,
5142
5814
  terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'agent'),
5143
5815
  result: { failedBy: 'agent' },
5816
+ ...(platformFeedback ? { platformFeedback } : {}),
5144
5817
  });
5145
5818
  (0, output_1.printSuccess)('Task marked failed. This was the final operation; send one short final status sentence and stop immediately.');
5146
5819
  }