@playdrop/playdrop-cli 0.12.42 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) 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/devServer.js +19 -0
  10. package/dist/commands/feedback.d.ts +2 -0
  11. package/dist/commands/feedback.js +44 -2
  12. package/dist/commands/upload.d.ts +24 -0
  13. package/dist/commands/upload.js +122 -4
  14. package/dist/commands/worker/instrument-evidence.js +1 -1
  15. package/dist/commands/worker/runtime.d.ts +14 -0
  16. package/dist/commands/worker/runtime.js +97 -8
  17. package/dist/commands/worker.d.ts +27 -2
  18. package/dist/commands/worker.js +688 -146
  19. package/dist/index.js +7 -0
  20. package/dist/listingPreflight.js +15 -4
  21. package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -2
  22. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  23. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +3 -2
  24. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +13 -2
  26. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts +2 -1
  29. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts.map +1 -1
  30. package/node_modules/@playdrop/api-client/dist/domains/me.js +11 -0
  31. package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -3
  32. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  33. package/node_modules/@playdrop/api-client/dist/index.js +14 -2
  34. package/node_modules/@playdrop/config/client-meta.json +2 -2
  35. package/node_modules/@playdrop/config/dist/src/constants.d.ts +1 -0
  36. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  37. package/node_modules/@playdrop/config/dist/src/constants.js +1 -0
  38. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  39. package/node_modules/@playdrop/types/dist/api.d.ts +195 -4
  40. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  41. package/node_modules/@playdrop/types/dist/api.js +25 -2
  42. package/node_modules/entities/decode-codepoint.d.ts +19 -0
  43. package/node_modules/entities/decode-codepoint.d.ts.map +1 -0
  44. package/node_modules/entities/decode-codepoint.js +77 -0
  45. package/node_modules/entities/decode-codepoint.js.map +1 -0
  46. package/node_modules/entities/decode.d.ts +209 -0
  47. package/node_modules/entities/decode.d.ts.map +1 -0
  48. package/node_modules/entities/decode.js +511 -0
  49. package/node_modules/entities/decode.js.map +1 -0
  50. package/node_modules/entities/encode.d.ts +22 -0
  51. package/node_modules/entities/encode.d.ts.map +1 -0
  52. package/node_modules/entities/encode.js +73 -0
  53. package/node_modules/entities/encode.js.map +1 -0
  54. package/node_modules/entities/escape.d.ts +43 -0
  55. package/node_modules/entities/escape.d.ts.map +1 -0
  56. package/node_modules/entities/escape.js +121 -0
  57. package/node_modules/entities/escape.js.map +1 -0
  58. package/node_modules/entities/generated/decode-data-html.d.ts +2 -0
  59. package/node_modules/entities/generated/decode-data-html.d.ts.map +1 -0
  60. package/node_modules/entities/generated/decode-data-html.js +10 -0
  61. package/node_modules/entities/generated/decode-data-html.js.map +1 -0
  62. package/node_modules/entities/generated/decode-data-xml.d.ts +2 -0
  63. package/node_modules/entities/generated/decode-data-xml.d.ts.map +1 -0
  64. package/node_modules/entities/generated/decode-data-xml.js +10 -0
  65. package/node_modules/entities/generated/decode-data-xml.js.map +1 -0
  66. package/node_modules/entities/generated/encode-html.d.ts +8 -0
  67. package/node_modules/entities/generated/encode-html.d.ts.map +1 -0
  68. package/node_modules/entities/generated/encode-html.js +13 -0
  69. package/node_modules/entities/generated/encode-html.js.map +1 -0
  70. package/node_modules/entities/index.d.ts +96 -0
  71. package/node_modules/entities/index.d.ts.map +1 -0
  72. package/node_modules/entities/index.js +131 -0
  73. package/node_modules/entities/index.js.map +1 -0
  74. package/node_modules/entities/package.json +3 -0
  75. package/node_modules/parse5/common/doctype.d.ts +4 -0
  76. package/node_modules/parse5/common/doctype.js +118 -0
  77. package/node_modules/parse5/common/error-codes.d.ts +67 -0
  78. package/node_modules/parse5/common/error-codes.js +66 -0
  79. package/node_modules/parse5/common/foreign-content.d.ts +9 -0
  80. package/node_modules/parse5/common/foreign-content.js +237 -0
  81. package/node_modules/parse5/common/html.d.ts +289 -0
  82. package/node_modules/parse5/common/html.js +528 -0
  83. package/node_modules/parse5/common/token.d.ts +84 -0
  84. package/node_modules/parse5/common/token.js +24 -0
  85. package/node_modules/parse5/common/unicode.d.ts +42 -0
  86. package/node_modules/parse5/common/unicode.js +70 -0
  87. package/node_modules/parse5/index.d.ts +71 -0
  88. package/node_modules/parse5/index.js +54 -0
  89. package/node_modules/parse5/package.json +1 -0
  90. package/node_modules/parse5/parser/formatting-element-list.d.ts +36 -0
  91. package/node_modules/parse5/parser/formatting-element-list.js +114 -0
  92. package/node_modules/parse5/parser/index.d.ts +221 -0
  93. package/node_modules/parse5/parser/index.js +3240 -0
  94. package/node_modules/parse5/parser/open-element-stack.d.ts +53 -0
  95. package/node_modules/parse5/parser/open-element-stack.js +328 -0
  96. package/node_modules/parse5/serializer/index.d.ts +60 -0
  97. package/node_modules/parse5/serializer/index.js +172 -0
  98. package/node_modules/parse5/tokenizer/index.d.ts +247 -0
  99. package/node_modules/parse5/tokenizer/index.js +2714 -0
  100. package/node_modules/parse5/tokenizer/preprocessor.d.ts +36 -0
  101. package/node_modules/parse5/tokenizer/preprocessor.js +200 -0
  102. package/node_modules/parse5/tree-adapters/default.d.ts +84 -0
  103. package/node_modules/parse5/tree-adapters/default.js +176 -0
  104. package/node_modules/parse5/tree-adapters/interface.d.ts +255 -0
  105. package/node_modules/parse5/tree-adapters/interface.js +2 -0
  106. package/package.json +7 -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;
@@ -71,6 +77,7 @@ exports.showWorkerStatus = showWorkerStatus;
71
77
  exports.setupWorker = setupWorker;
72
78
  exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
73
79
  exports.enforceInstrumentBrowserHygiene = enforceInstrumentBrowserHygiene;
80
+ exports.assignmentRequiresRegisteredBundle = assignmentRequiresRegisteredBundle;
74
81
  exports.startWorker = startWorker;
75
82
  exports.reportTask = reportTask;
76
83
  exports.showTaskContext = showTaskContext;
@@ -110,13 +117,17 @@ var runtime_2 = require("./worker/runtime");
110
117
  Object.defineProperty(exports, "assertWorkerTokenUsageWithinCap", { enumerable: true, get: function () { return runtime_2.assertWorkerTokenUsageWithinCap; } });
111
118
  Object.defineProperty(exports, "buildClaudeExecArgs", { enumerable: true, get: function () { return runtime_2.buildClaudeExecArgs; } });
112
119
  Object.defineProperty(exports, "buildClaudePermissionSettings", { enumerable: true, get: function () { return runtime_2.buildClaudePermissionSettings; } });
120
+ Object.defineProperty(exports, "buildCleanRoomChildEnv", { enumerable: true, get: function () { return runtime_2.buildCleanRoomChildEnv; } });
113
121
  Object.defineProperty(exports, "buildCodexExecArgs", { enumerable: true, get: function () { return runtime_2.buildCodexExecArgs; } });
114
122
  Object.defineProperty(exports, "buildPlaywrightMcpCommand", { enumerable: true, get: function () { return runtime_2.buildPlaywrightMcpCommand; } });
115
123
  Object.defineProperty(exports, "buildWorkerChildEnv", { enumerable: true, get: function () { return runtime_2.buildWorkerChildEnv; } });
116
124
  Object.defineProperty(exports, "DEFAULT_CODEX_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CODEX_TIMEOUT_MS; } });
125
+ Object.defineProperty(exports, "DEFAULT_CREATOR_AGENT_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CREATOR_AGENT_TIMEOUT_MS; } });
126
+ Object.defineProperty(exports, "DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS; } });
117
127
  Object.defineProperty(exports, "DEFAULT_WORKER_TOKEN_CAP", { enumerable: true, get: function () { return runtime_2.DEFAULT_WORKER_TOKEN_CAP; } });
118
128
  Object.defineProperty(exports, "extractAgentTokenUsage", { enumerable: true, get: function () { return runtime_2.extractAgentTokenUsage; } });
119
129
  Object.defineProperty(exports, "extractCodexRolloutTokenUsage", { enumerable: true, get: function () { return runtime_2.extractCodexRolloutTokenUsage; } });
130
+ Object.defineProperty(exports, "extractClaudeSessionId", { enumerable: true, get: function () { return runtime_2.extractClaudeSessionId; } });
120
131
  Object.defineProperty(exports, "extractCodexThreadId", { enumerable: true, get: function () { return runtime_2.extractCodexThreadId; } });
121
132
  Object.defineProperty(exports, "extractCodexTokensUsed", { enumerable: true, get: function () { return runtime_2.extractCodexTokensUsed; } });
122
133
  Object.defineProperty(exports, "readCodexRolloutTokenUsageForThread", { enumerable: true, get: function () { return runtime_2.readCodexRolloutTokenUsageForThread; } });
@@ -137,7 +148,9 @@ const FAIL_REPORT_RETRY_DELAY_MS = 2000;
137
148
  const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
138
149
  const SLACK_API_BASE_URL = 'https://slack.com/api';
139
150
  const WORKER_WRAPPER_CONTRACT_VERSION = 2;
140
- const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
151
+ const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'STATIC_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
152
+ const GAME_CREATION_TASK_KINDS = new Set(['NEW_GAME', 'STATIC_GAME', 'REMIX_GAME', 'GAME_UPDATE']);
153
+ const AGENT_MANAGED_UPLOAD_TASK_KINDS = new Set(['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE']);
141
154
  const requireFromWorker = (0, node_module_1.createRequire)(__filename);
142
155
  const AGENT_BUNDLE_MANIFEST_PATH = 'agent-bundle.json';
143
156
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES = 1000;
@@ -148,6 +161,36 @@ const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
148
161
  const ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES = 22;
149
162
  const ZIP_MAX_COMMENT_BYTES = 0xffff;
150
163
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
164
+ function usesCreatorRecovery(kind) {
165
+ return GAME_CREATION_TASK_KINDS.has(kind);
166
+ }
167
+ function buildCreatorRecoveryPrompt(input) {
168
+ const prompt = input.prompt.trim();
169
+ if (!prompt) {
170
+ throw new Error('agent_task_assignment_prompt_missing');
171
+ }
172
+ if (!GAME_CREATION_TASK_KINDS.has(input.kind)) {
173
+ throw new Error(`agent_task_creation_recovery_unsupported:${input.kind}`);
174
+ }
175
+ const finishInstruction = AGENT_MANAGED_UPLOAD_TASK_KINDS.has(input.kind)
176
+ ? 'Prioritize `playdrop task upload` and then `playdrop task done`.'
177
+ : 'Finish a valid standalone `index.html` at the project root, then stop so the worker can publish it.';
178
+ return [
179
+ 'This task was paused because it was taking too long. Continue from the existing workspace and do not restart or redesign the game.',
180
+ 'You have 10 minutes to finish the strongest solid version already present. Make only the smallest fixes required for validation and publishing.',
181
+ `${finishInstruction} Do not add features or polish. If completion cannot succeed, report the exact blocker and stop.`,
182
+ `Original request: ${prompt}`,
183
+ ].join('\n\n');
184
+ }
185
+ function buildGameEvalRetryPrompt(prompt) {
186
+ return [
187
+ prompt,
188
+ '',
189
+ 'This is a retry in the existing evaluation workspace.',
190
+ 'Reuse the existing result.json and evidence. Do not replay completed game checks unless required evidence is missing or invalid.',
191
+ 'First correct any deterministic submission-contract error, then submit the existing result and evidence and finish.',
192
+ ].join('\n');
193
+ }
151
194
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
152
195
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
153
196
  ['task', 'context'],
@@ -505,6 +548,29 @@ async function pruneWorkerTaskWorkspaces(incomingWorkspaceDir, retention = expor
505
548
  }
506
549
  return removed;
507
550
  }
551
+ async function prepareClaimedTaskWorkspace(input) {
552
+ await pruneWorkerTaskWorkspaces(input.workspaceDir);
553
+ const resumed = input.attempt > 1 && (0, node_fs_1.existsSync)(input.workspaceDir);
554
+ if (!resumed) {
555
+ await (0, promises_1.rm)(input.workspaceDir, { recursive: true, force: true });
556
+ await (0, promises_1.mkdir)(input.workspaceDir, { recursive: true });
557
+ return { resumed: false };
558
+ }
559
+ await removeAgentRunCredentials(input.workspaceDir);
560
+ const retainedProjectBindings = [
561
+ node_path_1.default.join(input.workspaceDir, '.playdrop.json'),
562
+ ...(0, node_fs_1.readdirSync)(input.workspaceDir, { withFileTypes: true })
563
+ .filter((entry) => entry.isDirectory() && entry.name !== '.playdrop')
564
+ .map((entry) => node_path_1.default.join(input.workspaceDir, entry.name, '.playdrop.json')),
565
+ ];
566
+ await Promise.all([
567
+ (0, promises_1.rm)(node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-events'), { recursive: true, force: true }),
568
+ (0, promises_1.rm)(node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-upload.lock'), { force: true }),
569
+ (0, promises_1.rm)(node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-upload-result.json'), { force: true }),
570
+ ...retainedProjectBindings.map((file) => (0, promises_1.rm)(file, { force: true })),
571
+ ]);
572
+ return { resumed: true };
573
+ }
508
574
  function buildWorkerTaskContext(input) {
509
575
  const playdrop = input.metadata.playdrop;
510
576
  if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
@@ -512,7 +578,7 @@ function buildWorkerTaskContext(input) {
512
578
  }
513
579
  const metadata = playdrop;
514
580
  const kind = metadata.kind;
515
- if (kind !== 'NEW_GAME' && kind !== 'REMIX_GAME' && kind !== 'GAME_UPDATE' && kind !== 'GAME_REVIEW' && kind !== 'GAME_EVAL') {
581
+ if (kind !== 'NEW_GAME' && kind !== 'STATIC_GAME' && kind !== 'REMIX_GAME' && kind !== 'GAME_UPDATE' && kind !== 'GAME_REVIEW' && kind !== 'GAME_EVAL') {
516
582
  throw new Error('agent_task_context_kind_missing');
517
583
  }
518
584
  const creatorUsername = typeof metadata.creatorUsername === 'string'
@@ -583,6 +649,23 @@ function resolveWorkerInstrumentSurface(taskContext) {
583
649
  return null;
584
650
  }
585
651
  const metadata = playdrop;
652
+ if (taskContext.kind === 'STATIC_GAME') {
653
+ const surfaceContext = metadata.surfaceContext;
654
+ if (!surfaceContext || typeof surfaceContext !== 'object' || Array.isArray(surfaceContext)) {
655
+ return null;
656
+ }
657
+ const context = surfaceContext;
658
+ if (context.deviceClass === 'DESKTOP') {
659
+ return 'DESKTOP';
660
+ }
661
+ if (context.orientation === 'PORTRAIT') {
662
+ return 'MOBILE_PORTRAIT';
663
+ }
664
+ if (context.orientation === 'LANDSCAPE') {
665
+ return 'MOBILE_LANDSCAPE';
666
+ }
667
+ return null;
668
+ }
586
669
  if (taskContext.kind === 'GAME_REVIEW') {
587
670
  const reviewTarget = metadata.reviewTarget;
588
671
  if (!reviewTarget || typeof reviewTarget !== 'object' || Array.isArray(reviewTarget)) {
@@ -1073,17 +1156,19 @@ function readAgentBundleManifest(extractedRoot) {
1073
1156
  }
1074
1157
  const manifest = parsed;
1075
1158
  if (manifest.version !== 1
1076
- || !manifest.plugin
1077
- || typeof manifest.plugin.root !== 'string'
1159
+ || !Object.prototype.hasOwnProperty.call(manifest, 'plugin')
1160
+ || (manifest.plugin !== null && typeof manifest.plugin?.root !== 'string')
1078
1161
  || !manifest.workspace
1079
1162
  || !Array.isArray(manifest.workspace.files)) {
1080
1163
  throw new Error('agent_bundle_manifest_invalid');
1081
1164
  }
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'))) {
1165
+ const pluginRoot = manifest.plugin === null
1166
+ ? null
1167
+ : normalizeAgentBundlePath(extractedRoot, manifest.plugin?.root, 'agent_bundle_plugin_root_invalid');
1168
+ if (pluginRoot && !isFile(node_path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json'))) {
1084
1169
  throw new Error('agent_bundle_codex_plugin_manifest_missing');
1085
1170
  }
1086
- if (!isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))) {
1171
+ if (pluginRoot && !isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))) {
1087
1172
  throw new Error('agent_bundle_claude_plugin_manifest_missing');
1088
1173
  }
1089
1174
  const destinations = new Set();
@@ -1146,6 +1231,19 @@ async function stageAgentBundleWorkspaceFiles(extractedRoot, workspaceDir, taskK
1146
1231
  }
1147
1232
  return resolved.pluginRoot;
1148
1233
  }
1234
+ async function stageDevPluginWorkingTree(input) {
1235
+ const sourceRoot = node_path_1.default.resolve(input.workingTree);
1236
+ readAgentBundleManifest(sourceRoot);
1237
+ const snapshotRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'dev-task-plugin');
1238
+ await (0, promises_1.rm)(snapshotRoot, { recursive: true, force: true });
1239
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(snapshotRoot), { recursive: true });
1240
+ await (0, promises_1.cp)(sourceRoot, snapshotRoot, { recursive: true });
1241
+ const pluginRoot = await stageAgentBundleWorkspaceFiles(snapshotRoot, input.workspaceDir, input.taskKind);
1242
+ if (!pluginRoot) {
1243
+ throw new Error('worker_dev_plugin_working_tree_requires_plugin');
1244
+ }
1245
+ return pluginRoot;
1246
+ }
1149
1247
  async function stageAssignmentPluginBundle(input) {
1150
1248
  const sha256 = typeof input.pluginBundle.sha256 === 'string' ? input.pluginBundle.sha256.trim() : '';
1151
1249
  const fileName = typeof input.pluginBundle.fileName === 'string' ? input.pluginBundle.fileName.trim() : '';
@@ -1168,7 +1266,7 @@ async function stageAssignmentPluginBundle(input) {
1168
1266
  throw new Error('agent_task_plugin_bundle_sha_mismatch');
1169
1267
  }
1170
1268
  assertAssignmentPluginBundleExtractionBounds(buffer);
1171
- const pluginRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-plugin');
1269
+ const pluginRoot = node_path_1.default.join(input.extractionDir ?? input.workspaceDir, '.playdrop', 'task-plugin');
1172
1270
  await (0, promises_1.rm)(pluginRoot, { recursive: true, force: true });
1173
1271
  await (0, promises_1.mkdir)(pluginRoot, { recursive: true });
1174
1272
  const entries = (0, fflate_1.unzipSync)(new Uint8Array(buffer));
@@ -1215,7 +1313,9 @@ function resolveDevPluginWorkingTree(input) {
1215
1313
  throw new Error(`worker_dev_plugin_working_tree_requires_dev_environment:${input.env}`);
1216
1314
  }
1217
1315
  const resolved = node_path_1.default.resolve(workingTree);
1218
- readAgentBundleManifest(resolved);
1316
+ if (!readAgentBundleManifest(resolved).pluginRoot) {
1317
+ throw new Error('worker_dev_plugin_working_tree_requires_plugin');
1318
+ }
1219
1319
  return resolved;
1220
1320
  }
1221
1321
  // A PlayDrop project root is the topmost directory holding a catalogue.json,
@@ -1900,11 +2000,15 @@ async function prepareCodexRunHomes(input) {
1900
2000
  const runRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'codex-run');
1901
2001
  const homeDir = node_path_1.default.join(runRoot, 'home');
1902
2002
  const codexHomeDir = node_path_1.default.join(runRoot, 'codex-home');
1903
- await (0, promises_1.rm)(runRoot, { recursive: true, force: true });
2003
+ if (!input.preserveExisting) {
2004
+ await (0, promises_1.rm)(runRoot, { recursive: true, force: true });
2005
+ }
1904
2006
  await (0, promises_1.mkdir)(runRoot, { recursive: true, mode: 0o700 });
1905
2007
  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 });
2008
+ await (0, promises_1.mkdir)(homeDir, { recursive: true, mode: 0o700 });
2009
+ await (0, promises_1.chmod)(homeDir, 0o700);
2010
+ await (0, promises_1.mkdir)(codexHomeDir, { recursive: true, mode: 0o700 });
2011
+ await (0, promises_1.chmod)(codexHomeDir, 0o700);
1908
2012
  const sourceCodexHome = node_path_1.default.resolve(input.sourceCodexHome?.trim()
1909
2013
  || node_process_1.default.env.CODEX_HOME?.trim()
1910
2014
  || node_path_1.default.join(node_os_1.default.homedir(), '.codex'));
@@ -1915,12 +2019,14 @@ async function prepareCodexRunHomes(input) {
1915
2019
  const stagedAuthPath = node_path_1.default.join(codexHomeDir, 'auth.json');
1916
2020
  await (0, promises_1.copyFile)(sourceAuthPath, stagedAuthPath);
1917
2021
  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
- });
2022
+ if (input.stagePlaydropSession !== false) {
2023
+ await stageWorkerPlaydropSession({
2024
+ homeDir,
2025
+ workerUsername: input.workerUsername,
2026
+ envName: input.envName,
2027
+ playdropConfig: input.playdropConfig,
2028
+ });
2029
+ }
1924
2030
  return { homeDir, codexHomeDir };
1925
2031
  }
1926
2032
  async function stageWorkerPlaydropSession(input) {
@@ -1999,11 +2105,13 @@ async function removeStaleAgentRunCredentials(workerHomeDir = resolveWorkerHomeD
1999
2105
  }
2000
2106
  async function runCodex(input) {
2001
2107
  const { homeDir, codexHomeDir } = await prepareCodexRunHomes({
2002
- workspaceDir: input.workspaceDir,
2108
+ workspaceDir: input.stateDir ?? input.workspaceDir,
2003
2109
  workerUsername: input.workerUsername,
2004
2110
  envName: input.envName,
2111
+ stagePlaydropSession: !input.cleanRoom,
2112
+ preserveExisting: Boolean(input.resumeSessionId),
2005
2113
  });
2006
- if (input.taskPluginRoot) {
2114
+ if (input.taskPluginRoot && !input.resumeSessionId) {
2007
2115
  const pluginEnv = {
2008
2116
  ...node_process_1.default.env,
2009
2117
  HOME: homeDir,
@@ -2018,7 +2126,7 @@ async function runCodex(input) {
2018
2126
  env: pluginEnv,
2019
2127
  });
2020
2128
  }
2021
- if (input.enablePlaywrightMcp) {
2129
+ if (input.enablePlaywrightMcp && !input.resumeSessionId) {
2022
2130
  const server = (0, runtime_1.buildPlaywrightMcpCommand)(input.instrumentSurface);
2023
2131
  await execFileAsync('codex', ['mcp', 'add', 'playwright', '--', server.command, ...server.args], {
2024
2132
  cwd: input.workspaceDir,
@@ -2040,12 +2148,17 @@ async function runCodex(input) {
2040
2148
  workspaceDir: input.workspaceDir,
2041
2149
  model: input.codexModel.model,
2042
2150
  reasoningEffort: input.codexModel.reasoningEffort,
2043
- networkAccess: (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_AGENT_NETWORK_ACCESS', false),
2151
+ networkAccess: input.cleanRoom || (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_AGENT_NETWORK_ACCESS', false),
2044
2152
  sandboxMode: (0, runtime_1.readCodexSandboxMode)(),
2045
2153
  imagePaths: input.imagePaths,
2154
+ resumeSessionId: input.resumeSessionId,
2046
2155
  }),
2047
2156
  cwd: input.workspaceDir,
2048
- env: (0, runtime_1.buildWorkerChildEnv)({
2157
+ env: input.cleanRoom ? (0, runtime_1.buildCleanRoomChildEnv)({
2158
+ binDir: input.binDir,
2159
+ homeDir,
2160
+ codexHomeDir,
2161
+ }) : (0, runtime_1.buildWorkerChildEnv)({
2049
2162
  binDir: input.binDir,
2050
2163
  taskId: input.taskId,
2051
2164
  taskToken: input.taskToken,
@@ -2058,7 +2171,7 @@ async function runCodex(input) {
2058
2171
  playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
2059
2172
  }),
2060
2173
  stdin: input.prompt,
2061
- timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CODEX_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2174
+ timeoutMs: input.timeoutMs ?? (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CODEX_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2062
2175
  maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
2063
2176
  onTranscriptChunks: async (chunks) => {
2064
2177
  if (!observedCodexThreadId) {
@@ -2080,16 +2193,17 @@ async function runCodex(input) {
2080
2193
  if (result.tokenUsage.usageParseError) {
2081
2194
  console.warn('Codex thread id was not observed; rollout token usage could not be read.');
2082
2195
  }
2083
- return result;
2196
+ return { ...result, providerSessionId: null };
2084
2197
  }
2085
2198
  const rolloutUsage = await (0, runtime_1.readCodexRolloutTokenUsageForThread)(observedCodexThreadId, {
2086
2199
  sessionsRoot: node_path_1.default.join(codexHomeDir, 'sessions'),
2200
+ deleteAfterRead: false,
2087
2201
  });
2088
2202
  if (rolloutUsage.usageParseError || rolloutUsage.totalTokens === null) {
2089
2203
  if (result.tokenUsage.usageParseError) {
2090
2204
  console.warn(`Codex rollout token usage could not be read for thread ${observedCodexThreadId}: ${rolloutUsage.usageParseError ?? 'unknown_error'}.`);
2091
2205
  }
2092
- return result;
2206
+ return { ...result, providerSessionId: observedCodexThreadId };
2093
2207
  }
2094
2208
  if (!result.tokenUsage.usageParseError
2095
2209
  && result.tokenUsage.totalTokens !== null
@@ -2098,20 +2212,25 @@ async function runCodex(input) {
2098
2212
  }
2099
2213
  return {
2100
2214
  ...result,
2215
+ providerSessionId: observedCodexThreadId,
2101
2216
  tokenUsage: rolloutUsage,
2102
2217
  tokensUsed: rolloutUsage.totalTokens,
2103
2218
  };
2104
2219
  }
2105
2220
  async function runClaude(input) {
2106
- const { playdropConfigPath } = await prepareClaudeRunConfig({
2107
- workspaceDir: input.workspaceDir,
2108
- workerUsername: input.workerUsername,
2109
- envName: input.envName,
2110
- });
2221
+ const playdropConfigPath = input.cleanRoom
2222
+ ? null
2223
+ : (await prepareClaudeRunConfig({
2224
+ workspaceDir: input.stateDir ?? input.workspaceDir,
2225
+ workerUsername: input.workerUsername,
2226
+ envName: input.envName,
2227
+ })).playdropConfigPath;
2111
2228
  const denyReadRoots = resolveClaudeDenyReadRoots(input.workspaceDir);
2112
2229
  const screenshotCollector = input.enableChrome || input.enablePlaywrightMcp
2113
2230
  ? (0, instrument_evidence_1.createWorkerInstrumentScreenshotCollector)(input.workspaceDir)
2114
2231
  : null;
2232
+ let observedClaudeSessionId = input.resumeSessionId?.trim() || null;
2233
+ let claudeSessionProbe = '';
2115
2234
  let playwrightMcpConfigPath = null;
2116
2235
  if (input.enablePlaywrightMcp) {
2117
2236
  playwrightMcpConfigPath = node_path_1.default.join(input.workspaceDir, '.playdrop', 'claude-run', 'playwright-mcp.json');
@@ -2125,12 +2244,16 @@ async function runClaude(input) {
2125
2244
  workspaceDir: input.workspaceDir,
2126
2245
  denyReadRoots,
2127
2246
  pluginDir: input.taskPluginRoot,
2247
+ playdropCliEnabled: !input.cleanRoom,
2128
2248
  chrome: input.enableChrome,
2129
2249
  playwrightMcp: input.enablePlaywrightMcp,
2130
2250
  playwrightMcpConfigPath,
2251
+ resumeSessionId: input.resumeSessionId,
2131
2252
  }),
2132
2253
  cwd: input.workspaceDir,
2133
- env: (0, runtime_1.buildWorkerChildEnv)({
2254
+ env: input.cleanRoom ? (0, runtime_1.buildCleanRoomChildEnv)({
2255
+ binDir: input.binDir,
2256
+ }) : (0, runtime_1.buildWorkerChildEnv)({
2134
2257
  binDir: input.binDir,
2135
2258
  taskId: input.taskId,
2136
2259
  taskToken: input.taskToken,
@@ -2138,7 +2261,7 @@ async function runClaude(input) {
2138
2261
  envName: input.envName,
2139
2262
  eventDir: input.eventDir,
2140
2263
  devPort: input.devPort,
2141
- playdropConfigPath,
2264
+ playdropConfigPath: playdropConfigPath,
2142
2265
  playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
2143
2266
  }),
2144
2267
  stdin: `${JSON.stringify({
@@ -2158,9 +2281,15 @@ async function runClaude(input) {
2158
2281
  ],
2159
2282
  },
2160
2283
  })}\n`,
2161
- timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2284
+ timeoutMs: input.timeoutMs ?? (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2162
2285
  maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
2163
- onTranscriptChunks: input.onTranscriptChunks,
2286
+ onTranscriptChunks: async (chunks) => {
2287
+ if (!observedClaudeSessionId) {
2288
+ claudeSessionProbe = (claudeSessionProbe + chunks.map((chunk) => chunk.content).join('')).slice(-64000);
2289
+ observedClaudeSessionId = (0, runtime_1.extractClaudeSessionId)(claudeSessionProbe);
2290
+ }
2291
+ await input.onTranscriptChunks(chunks);
2292
+ },
2164
2293
  ...(screenshotCollector
2165
2294
  ? { onStdoutText: (text) => screenshotCollector.push(text) > 0 }
2166
2295
  : {}),
@@ -2168,7 +2297,9 @@ async function runClaude(input) {
2168
2297
  onChild: input.onChild,
2169
2298
  });
2170
2299
  screenshotCollector?.finish();
2171
- return result;
2300
+ observedClaudeSessionId = observedClaudeSessionId
2301
+ ?? (0, runtime_1.extractClaudeSessionId)(`${result.stdout}\n${result.stderr}\n${result.outputTail}`);
2302
+ return { ...result, providerSessionId: observedClaudeSessionId };
2172
2303
  }
2173
2304
  function isPathInside(parent, child) {
2174
2305
  const relative = node_path_1.default.relative(parent, child);
@@ -2199,6 +2330,35 @@ function resolveClaudeDenyReadRoots(workspaceDir) {
2199
2330
  }
2200
2331
  // Raw stdout/stderr tails stay out of task results on purpose: transcripts are
2201
2332
  // streamed separately and results can surface to creators (spec section 11).
2333
+ function mergeCreatorRecoveryResults(runtime, initial, recovery) {
2334
+ const sumNullable = (left, right) => (left === null && right === null ? null : (left ?? 0) + (right ?? 0));
2335
+ const tokenUsage = runtime === 'CODEX' && recovery.tokenUsage.totalTokens !== null
2336
+ ? recovery.tokenUsage
2337
+ : {
2338
+ inputTokens: sumNullable(initial.tokenUsage.inputTokens, recovery.tokenUsage.inputTokens),
2339
+ outputTokens: sumNullable(initial.tokenUsage.outputTokens, recovery.tokenUsage.outputTokens),
2340
+ cacheCreationInputTokens: sumNullable(initial.tokenUsage.cacheCreationInputTokens, recovery.tokenUsage.cacheCreationInputTokens),
2341
+ cacheReadInputTokens: sumNullable(initial.tokenUsage.cacheReadInputTokens, recovery.tokenUsage.cacheReadInputTokens),
2342
+ totalTokens: sumNullable(initial.tokenUsage.totalTokens, recovery.tokenUsage.totalTokens),
2343
+ rawProviderUsage: {
2344
+ initial: initial.tokenUsage.rawProviderUsage,
2345
+ recovery: recovery.tokenUsage.rawProviderUsage,
2346
+ },
2347
+ usageParseError: initial.tokenUsage.totalTokens === null || recovery.tokenUsage.totalTokens === null
2348
+ ? 'creator_recovery_usage_incomplete'
2349
+ : null,
2350
+ };
2351
+ const recoveryMarker = recovery.timedOut ? '\nagent_task_creation_recovery_timeout' : '';
2352
+ return {
2353
+ ...recovery,
2354
+ stdout: `${initial.stdout}\n${recovery.stdout}`.slice(-runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS),
2355
+ stderr: `${initial.stderr}\n${recovery.stderr}${recoveryMarker}`.slice(-runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS),
2356
+ outputTail: `${initial.outputTail}\n${recovery.outputTail}${recoveryMarker}`.slice(-runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS),
2357
+ tokenUsage,
2358
+ tokensUsed: tokenUsage.totalTokens,
2359
+ providerSessionId: recovery.providerSessionId ?? initial.providerSessionId ?? null,
2360
+ };
2361
+ }
2202
2362
  function buildAgentRunResult(result) {
2203
2363
  return {
2204
2364
  exitCode: result.exitCode,
@@ -2326,6 +2486,12 @@ function extractProviderStatusCode(text) {
2326
2486
  function buildAgentFailureCode(agent, result, request) {
2327
2487
  const provider = providerNameForAgent(agent);
2328
2488
  const combinedOutput = `${result.outputTail}\n${result.stderr}\n${result.stdout}`;
2489
+ if (combinedOutput.includes('agent_task_resume_session_missing')) {
2490
+ return `agent_task_resume_session_missing:${provider}`;
2491
+ }
2492
+ if (combinedOutput.includes('agent_task_creation_recovery_timeout')) {
2493
+ return `agent_task_creation_recovery_timeout:${provider}`;
2494
+ }
2329
2495
  if (request
2330
2496
  && (/\b(?:unknown|invalid|unsupported)\s+(?:model|effort)\b/i.test(combinedOutput)
2331
2497
  || /\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 +2529,12 @@ function describeAgentFailureForEvent(errorCode) {
2363
2529
  if (code === 'agent_provider_timeout') {
2364
2530
  return `${providerLabel} timed out before completing the task. Retry this task when the provider is healthy.`;
2365
2531
  }
2532
+ if (code === 'agent_task_resume_session_missing') {
2533
+ return `${providerLabel} timed out, but the worker could not recover the same conversation. The task was stopped without starting over.`;
2534
+ }
2535
+ if (code === 'agent_task_creation_recovery_timeout') {
2536
+ return `${providerLabel} did not finish during the final creation recovery window, so the task was stopped permanently.`;
2537
+ }
2366
2538
  if (code === 'worker_shutdown') {
2367
2539
  return 'The worker stopped while running this task. Start the worker again, then retry the task.';
2368
2540
  }
@@ -2422,6 +2594,24 @@ async function createLocalPlaydropShim(input) {
2422
2594
  }
2423
2595
  return binDir;
2424
2596
  }
2597
+ async function createStaticGameCleanToolPath(workspaceDir, runtime) {
2598
+ const binDir = node_path_1.default.join(workspaceDir, '.clean-tools');
2599
+ await (0, promises_1.rm)(binDir, { recursive: true, force: true });
2600
+ await (0, promises_1.mkdir)(binDir, { recursive: true });
2601
+ const commands = runtime === 'CODEX'
2602
+ ? ['codex', 'node', 'npm', 'npx']
2603
+ : ['claude', 'node', 'npm', 'npx'];
2604
+ for (const command of commands) {
2605
+ const target = command === 'node'
2606
+ ? node_process_1.default.execPath
2607
+ : (await execFileAsync('/usr/bin/which', [command])).stdout.trim();
2608
+ if (!target || !node_path_1.default.isAbsolute(target)) {
2609
+ throw new Error(`static_game_clean_tool_missing:${command}`);
2610
+ }
2611
+ (0, node_fs_1.symlinkSync)(target, node_path_1.default.join(binDir, command));
2612
+ }
2613
+ return binDir;
2614
+ }
2425
2615
  function probeCodexInstallation() {
2426
2616
  const probe = (0, shellProbe_1.runShell)((0, shellProbe_1.buildCommandPathProbe)('codex'));
2427
2617
  if (probe.exitCode !== 0 || !probe.output.trim()) {
@@ -3799,7 +3989,7 @@ on run matchers
3799
3989
  return "remaining=" & remainingCount & ";blank=" & blankWindowCount
3800
3990
  end run
3801
3991
  `;
3802
- async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3992
+ async function enforceInstrumentBrowserHygieneUnlocked(assignment, options = {}) {
3803
3993
  if (assignment.agent.browser === 'NONE') {
3804
3994
  return { remainingTaskTabs: 0, blankWindows: 0 };
3805
3995
  }
@@ -3814,13 +4004,22 @@ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3814
4004
  const result = await execFileAsync(command, args, { timeout: 15000, maxBuffer: 1024 * 1024 });
3815
4005
  return { stdout: result.stdout, stderr: result.stderr };
3816
4006
  });
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)}`);
4007
+ let result = null;
4008
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
4009
+ try {
4010
+ result = await runner('osascript', ['-e', INSTRUMENT_BROWSER_HYGIENE_APPLESCRIPT, ...matchers]);
4011
+ break;
4012
+ }
4013
+ catch (error) {
4014
+ const message = error instanceof Error ? error.message : String(error);
4015
+ if (!message.includes('Connection is invalid. (-609)') || attempt === 3) {
4016
+ throw new Error(`worker_browser_hygiene_automation_failed:${message}`);
4017
+ }
4018
+ await sleep(250);
4019
+ }
3823
4020
  }
4021
+ if (!result)
4022
+ throw new Error('worker_browser_hygiene_automation_failed:no_result');
3824
4023
  const match = /remaining=(\d+);blank=(\d+)/.exec(result.stdout.trim());
3825
4024
  if (!match) {
3826
4025
  throw new Error(`worker_browser_hygiene_invalid_result:${result.stdout.trim() || result.stderr.trim() || 'empty'}`);
@@ -3832,12 +4031,25 @@ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3832
4031
  }
3833
4032
  return { remainingTaskTabs, blankWindows };
3834
4033
  }
4034
+ let instrumentBrowserHygieneTail = Promise.resolve();
4035
+ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
4036
+ const result = instrumentBrowserHygieneTail.then(() => enforceInstrumentBrowserHygieneUnlocked(assignment, options));
4037
+ instrumentBrowserHygieneTail = result.then(() => undefined, () => undefined);
4038
+ return result;
4039
+ }
4040
+ function assignmentRequiresRegisteredBundle(assignment) {
4041
+ const playdropMetadata = assignment.task.metadata.playdrop;
4042
+ return Boolean(playdropMetadata
4043
+ && typeof playdropMetadata === 'object'
4044
+ && !Array.isArray(playdropMetadata)
4045
+ && playdropMetadata.bundleSource === 'REGISTERED');
4046
+ }
3835
4047
  function throwWorkerRuntimeError(error) {
3836
4048
  throw error;
3837
4049
  }
3838
4050
  async function startWorker(options = {}) {
3839
4051
  loadWorkerEnvFile(options.env);
3840
- await (0, commandContext_1.withEnvironment)('worker start', 'Starting the PlayDrop worker', async ({ client, env }) => {
4052
+ await (0, commandContext_1.withEnvironment)('worker start', 'Starting the PlayDrop worker', async ({ client, env, envConfig, token }) => {
3841
4053
  const devPluginWorkingTree = resolveDevPluginWorkingTree({
3842
4054
  workingTree: options.devPluginWorkingTree,
3843
4055
  env,
@@ -4033,6 +4245,7 @@ async function startWorker(options = {}) {
4033
4245
  let heartbeatObservedFinalizedTask = false;
4034
4246
  let retainWorkspace = false;
4035
4247
  let workspaceDir = null;
4248
+ let agentWorkspaceDir = null;
4036
4249
  let availableSkillPaths = [];
4037
4250
  const observedSkillPaths = new Set();
4038
4251
  const assignment = claimedAssignment;
@@ -4062,6 +4275,38 @@ async function startWorker(options = {}) {
4062
4275
  });
4063
4276
  telemetryReported = true;
4064
4277
  };
4278
+ const failUploadedArtifactWithoutTaskDone = async () => {
4279
+ const failureCode = 'agent_exited_after_upload_without_task_done';
4280
+ retainWorkspace = true;
4281
+ await client.workerCreateAgentTaskEvent(task.id, {
4282
+ workerKey,
4283
+ leaseToken,
4284
+ kind: 'progress',
4285
+ phase: 'failed',
4286
+ message: 'The agent uploaded an artifact but exited without marking the task done.',
4287
+ pct: null,
4288
+ payload: { error: failureCode },
4289
+ });
4290
+ await failTaskWithRetry(client, task.id, {
4291
+ workerKey,
4292
+ leaseToken,
4293
+ error: failureCode,
4294
+ terminalReason: {
4295
+ kind: 'failure',
4296
+ category: 'agent_failure',
4297
+ code: failureCode,
4298
+ message: 'The agent uploaded an artifact but did not complete the task protocol.',
4299
+ retryable: false,
4300
+ rawError: failureCode,
4301
+ source: 'agent',
4302
+ },
4303
+ result: {
4304
+ stage: 'completion',
4305
+ uploadedArtifactPreserved: true,
4306
+ },
4307
+ });
4308
+ await reportTelemetry('FAILED');
4309
+ };
4065
4310
  try {
4066
4311
  const codexModel = assignment.agent.runtime === 'CODEX'
4067
4312
  ? resolveCodexModel(assignment.agent.model, assignment.agent.reasoningEffort)
@@ -4071,24 +4316,35 @@ async function startWorker(options = {}) {
4071
4316
  : null;
4072
4317
  resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.agent.model;
4073
4318
  reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
4319
+ const cleanRoom = task.kind === 'STATIC_GAME';
4074
4320
  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({
4321
+ const preparedWorkspace = await prepareClaimedTaskWorkspace({
4087
4322
  workspaceDir,
4088
- taskId: taskContext.taskId,
4089
- attempt: taskContext.attempt,
4090
- devPort,
4323
+ attempt: task.attempts,
4091
4324
  });
4325
+ if (preparedWorkspace.resumed) {
4326
+ console.log(`Agent task ${task.id} retry resumed retained workspace attempt ${task.attempts}.`);
4327
+ }
4328
+ agentWorkspaceDir = cleanRoom ? node_path_1.default.join(workspaceDir, 'project') : workspaceDir;
4329
+ await (0, promises_1.mkdir)(agentWorkspaceDir, { recursive: true });
4330
+ const eventDir = node_path_1.default.join(workspaceDir, '.playdrop', 'task-events');
4331
+ await (0, promises_1.mkdir)(eventDir, { recursive: true });
4332
+ if (!cleanRoom) {
4333
+ await stageWorkerTaskContext({
4334
+ workspaceDir,
4335
+ env,
4336
+ devPort,
4337
+ taskContext,
4338
+ });
4339
+ }
4340
+ const binDir = cleanRoom
4341
+ ? await createStaticGameCleanToolPath(workspaceDir, assignment.agent.runtime)
4342
+ : await createLocalPlaydropShim({
4343
+ workspaceDir,
4344
+ taskId: taskContext.taskId,
4345
+ attempt: taskContext.attempt,
4346
+ devPort,
4347
+ });
4092
4348
  const handleEventDrainFailure = (error) => {
4093
4349
  const classification = classifyWorkerEventDrainError(error);
4094
4350
  if (classification === 'session_expired') {
@@ -4120,17 +4376,59 @@ async function startWorker(options = {}) {
4120
4376
  appendObservedSkillPathsFromTranscript(chunks);
4121
4377
  await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
4122
4378
  };
4123
- await stageAssignmentWorkspaceV2(workspaceDir, assignment.workspace);
4124
- const assignmentPluginRoot = devPluginWorkingTree
4125
- ? await stageAgentBundleWorkspaceFiles(devPluginWorkingTree, workspaceDir, task.kind)
4379
+ await stageAssignmentWorkspaceV2(agentWorkspaceDir, assignment.workspace);
4380
+ const assignmentPluginRoot = !cleanRoom
4381
+ && devPluginWorkingTree
4382
+ && !assignmentRequiresRegisteredBundle(assignment)
4383
+ ? await stageDevPluginWorkingTree({
4384
+ workingTree: devPluginWorkingTree,
4385
+ workspaceDir: agentWorkspaceDir,
4386
+ taskKind: task.kind,
4387
+ })
4126
4388
  : assignment.plugin
4127
4389
  ? await stageAssignmentPluginBundle({
4128
- workspaceDir,
4390
+ workspaceDir: agentWorkspaceDir,
4391
+ extractionDir: workspaceDir,
4129
4392
  pluginBundle: assignment.plugin,
4130
4393
  taskKind: task.kind,
4131
4394
  })
4132
4395
  : null;
4133
4396
  availableSkillPaths = listPluginSkillPaths(assignmentPluginRoot);
4397
+ if (cleanRoom) {
4398
+ const playdropMetadata = assignment.task.metadata.playdrop;
4399
+ const postAgentAction = playdropMetadata && typeof playdropMetadata === 'object' && !Array.isArray(playdropMetadata)
4400
+ ? playdropMetadata.postAgentAction
4401
+ : null;
4402
+ const publishInstruction = postAgentAction && typeof postAgentAction === 'object' && !Array.isArray(postAgentAction)
4403
+ ? postAgentAction
4404
+ : null;
4405
+ if (!assignment.plugin
4406
+ || assignmentPluginRoot !== null
4407
+ || assignment.agent.browser !== 'NONE'
4408
+ || publishInstruction?.type !== 'PUBLISH_STATIC_GAME'
4409
+ || publishInstruction.entryPoint !== 'index.html') {
4410
+ throw new Error('static_game_clean_room_contract_invalid');
4411
+ }
4412
+ availableSkillPaths = [];
4413
+ await client.workerCreateAgentTaskEvent(task.id, {
4414
+ workerKey,
4415
+ leaseToken,
4416
+ kind: 'system',
4417
+ phase: 'clean_room',
4418
+ message: 'Static game clean-room capability evidence recorded.',
4419
+ payload: {
4420
+ bundleHash: assignment.plugin.sha256,
4421
+ requestedModel: assignment.agent.model,
4422
+ resolvedModel: resolvedRuntimeModel,
4423
+ networkAccess: true,
4424
+ browser: assignment.agent.browser,
4425
+ availableSkills: [],
4426
+ pluginInstalled: false,
4427
+ playdropCliInPath: false,
4428
+ taskCredentialsExposed: false,
4429
+ },
4430
+ });
4431
+ }
4134
4432
  heartbeatTimer = setInterval(() => {
4135
4433
  client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
4136
4434
  .then((heartbeat) => {
@@ -4189,81 +4487,161 @@ async function startWorker(options = {}) {
4189
4487
  handleEventDrainFailure(error);
4190
4488
  });
4191
4489
  }, 1000);
4192
- const prompt = assignment.request.prompt;
4490
+ const gameCreationTask = usesCreatorRecovery(task.kind);
4491
+ const startsInCreationRecovery = gameCreationTask && preparedWorkspace.resumed;
4492
+ const prompt = startsInCreationRecovery
4493
+ ? buildCreatorRecoveryPrompt({
4494
+ prompt: assignment.request.prompt,
4495
+ kind: task.kind,
4496
+ })
4497
+ : task.kind === 'GAME_EVAL' && preparedWorkspace.resumed
4498
+ ? buildGameEvalRetryPrompt(assignment.request.prompt)
4499
+ : assignment.request.prompt;
4193
4500
  const imageAttachments = assignment.request.attachments
4194
4501
  .filter((attachment) => attachment.contentType.startsWith('image/'))
4195
4502
  .map((attachment) => ({
4196
- path: resolveWorkspaceFileDestination(workspaceDir, attachment.path),
4503
+ path: resolveWorkspaceFileDestination(agentWorkspaceDir, attachment.path),
4197
4504
  contentType: attachment.contentType,
4198
4505
  }));
4199
4506
  if (shuttingDown) {
4200
4507
  throw new Error('worker_shutdown');
4201
4508
  }
4509
+ const runAssignedAgent = async (input) => {
4510
+ if (assignment.agent.runtime === 'CODEX') {
4511
+ return runCodex({
4512
+ workspaceDir: agentWorkspaceDir,
4513
+ stateDir: cleanRoom ? workspaceDir : undefined,
4514
+ binDir,
4515
+ eventDir,
4516
+ prompt: input.prompt,
4517
+ envName: env,
4518
+ workerUsername: username,
4519
+ taskId: task.id,
4520
+ taskToken: assignment.task.token,
4521
+ attempt: taskContext.attempt,
4522
+ devPort,
4523
+ codexModel: codexModel ?? resolveCodexModel(assignment.agent.model),
4524
+ taskPluginRoot: assignmentPluginRoot,
4525
+ enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4526
+ instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4527
+ imagePaths: imageAttachments.map((attachment) => attachment.path),
4528
+ onTranscriptChunks: async (chunks) => {
4529
+ await appendTranscriptChunks(chunks);
4530
+ },
4531
+ onChild: (controls) => {
4532
+ activeTerminators.set(task.id, controls.terminate);
4533
+ },
4534
+ cleanRoom,
4535
+ timeoutMs: input.timeoutMs,
4536
+ resumeSessionId: input.resumeSessionId,
4537
+ });
4538
+ }
4539
+ if (assignment.agent.runtime === 'CLAUDE_CODE') {
4540
+ return runClaude({
4541
+ workspaceDir: agentWorkspaceDir,
4542
+ stateDir: cleanRoom ? workspaceDir : undefined,
4543
+ binDir,
4544
+ eventDir,
4545
+ prompt: input.prompt,
4546
+ enableChrome: assignment.agent.browser === 'CHROME',
4547
+ enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4548
+ instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4549
+ envName: env,
4550
+ workerUsername: username,
4551
+ taskId: task.id,
4552
+ taskToken: assignment.task.token,
4553
+ attempt: taskContext.attempt,
4554
+ devPort,
4555
+ claudeModel: claudeModel ?? resolveClaudeModel(assignment.agent.model),
4556
+ taskPluginRoot: assignmentPluginRoot,
4557
+ imageAttachments,
4558
+ onTranscriptChunks: async (chunks) => {
4559
+ await appendTranscriptChunks(chunks);
4560
+ },
4561
+ onChild: (controls) => {
4562
+ activeTerminators.set(task.id, controls.terminate);
4563
+ },
4564
+ cleanRoom,
4565
+ timeoutMs: input.timeoutMs,
4566
+ resumeSessionId: input.resumeSessionId,
4567
+ });
4568
+ }
4569
+ if (assignment.agent.runtime === 'CURSOR_COMPOSER') {
4570
+ await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4571
+ workerKey,
4572
+ leaseToken,
4573
+ attemptIndex: 0,
4574
+ reason: 'unavailable_runtime',
4575
+ message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4576
+ });
4577
+ fenced = true;
4578
+ throw new Error('worker_cursor_composer_runner_not_configured');
4579
+ }
4580
+ throw new Error(`unsupported_worker_agent:${assignment.agent.runtime}`);
4581
+ };
4202
4582
  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, {
4583
+ if (startsInCreationRecovery) {
4584
+ await client.workerCreateAgentTaskEvent(task.id, {
4256
4585
  workerKey,
4257
4586
  leaseToken,
4258
- attemptIndex: 0,
4259
- reason: 'unavailable_runtime',
4260
- message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4587
+ kind: 'progress',
4588
+ phase: 'creation_recovery',
4589
+ message: 'Resumed the retained game workspace with a focused creation recovery window.',
4261
4590
  });
4262
- fenced = true;
4263
- throw new Error('worker_cursor_composer_runner_not_configured');
4264
4591
  }
4265
- else {
4266
- throw new Error(`unsupported_worker_agent:${assignment.agent.runtime}`);
4592
+ agentResult = await runAssignedAgent({
4593
+ prompt,
4594
+ timeoutMs: startsInCreationRecovery
4595
+ ? runtime_1.DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS
4596
+ : gameCreationTask
4597
+ ? runtime_1.DEFAULT_CREATOR_AGENT_TIMEOUT_MS
4598
+ : undefined,
4599
+ });
4600
+ if (startsInCreationRecovery && agentResult.timedOut) {
4601
+ const marker = 'agent_task_creation_recovery_timeout';
4602
+ agentResult = {
4603
+ ...agentResult,
4604
+ stderr: `${agentResult.stderr}\n${marker}`.trim(),
4605
+ outputTail: `${agentResult.outputTail}\n${marker}`.trim(),
4606
+ };
4607
+ }
4608
+ if (gameCreationTask
4609
+ && !startsInCreationRecovery
4610
+ && agentResult.timedOut
4611
+ && !fenced
4612
+ && !shuttingDown) {
4613
+ const timedOutResult = agentResult;
4614
+ const refreshedAfterTimeout = await fetchTaskDetail(client, target, task.id);
4615
+ if (refreshedAfterTimeout.task.status === 'RUNNING'
4616
+ && !hasAgentTaskUploadedArtifact(refreshedAfterTimeout.task)) {
4617
+ const providerSessionId = timedOutResult.providerSessionId?.trim() ?? '';
4618
+ if (!providerSessionId) {
4619
+ const marker = 'agent_task_resume_session_missing';
4620
+ agentResult = {
4621
+ ...timedOutResult,
4622
+ stderr: `${timedOutResult.stderr}\n${marker}`.trim(),
4623
+ outputTail: `${timedOutResult.outputTail}\n${marker}`.trim(),
4624
+ };
4625
+ }
4626
+ else {
4627
+ await client.workerCreateAgentTaskEvent(task.id, {
4628
+ workerKey,
4629
+ leaseToken,
4630
+ kind: 'progress',
4631
+ phase: 'creation_recovery',
4632
+ message: 'The one-hour creation ceiling was reached. Resuming the same agent conversation to finish the existing game.',
4633
+ });
4634
+ const recoveryResult = await runAssignedAgent({
4635
+ prompt: buildCreatorRecoveryPrompt({
4636
+ prompt: assignment.request.prompt,
4637
+ kind: task.kind,
4638
+ }),
4639
+ timeoutMs: runtime_1.DEFAULT_CREATOR_UPLOAD_RECOVERY_TIMEOUT_MS,
4640
+ resumeSessionId: providerSessionId,
4641
+ });
4642
+ agentResult = mergeCreatorRecoveryResults(assignment.agent.runtime, timedOutResult, recoveryResult);
4643
+ }
4644
+ }
4267
4645
  }
4268
4646
  agentCompletedAt = new Date();
4269
4647
  await queueEventDrain().catch((error) => {
@@ -4309,16 +4687,8 @@ async function startWorker(options = {}) {
4309
4687
  await reportTelemetry(refreshed.task.status);
4310
4688
  }
4311
4689
  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');
4690
+ console.error(`Agent task ${task.id} uploaded an artifact but exited before task completion; failing it explicitly.`);
4691
+ await failUploadedArtifactWithoutTaskDone();
4322
4692
  }
4323
4693
  else {
4324
4694
  retainWorkspace = true;
@@ -4352,17 +4722,158 @@ async function startWorker(options = {}) {
4352
4722
  console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
4353
4723
  await reportTelemetry(refreshed.task.status);
4354
4724
  }
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.',
4725
+ else if (task.kind === 'STATIC_GAME') {
4726
+ const tokenCap = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_TOKEN_CAP', runtime_1.DEFAULT_WORKER_TOKEN_CAP);
4727
+ (0, runtime_1.assertWorkerTokenUsageWithinCap)({
4728
+ tokensUsed: agentResult.tokensUsed,
4729
+ tokenCap,
4364
4730
  });
4365
- await reportTelemetry('RUNNING');
4731
+ await reportTelemetry('DONE');
4732
+ try {
4733
+ let appId = refreshed.task.appId;
4734
+ let appVersionId = refreshed.task.appVersionId;
4735
+ let result;
4736
+ if (Number.isInteger(appId) && Number(appId) > 0
4737
+ && Number.isInteger(appVersionId) && Number(appVersionId) > 0) {
4738
+ result = {
4739
+ appId,
4740
+ appVersionId,
4741
+ appName: refreshed.task.claimedAppName,
4742
+ version: refreshed.task.outputVersion,
4743
+ completedBy: 'worker_static_game_retry',
4744
+ };
4745
+ await client.workerCreateAgentTaskEvent(task.id, {
4746
+ workerKey,
4747
+ leaseToken,
4748
+ kind: 'system',
4749
+ phase: 'static_game_publish_retry',
4750
+ message: 'Reused the static game version already linked to this task.',
4751
+ });
4752
+ }
4753
+ else {
4754
+ const primarySurface = resolveWorkerInstrumentSurface(taskContext);
4755
+ if (!primarySurface) {
4756
+ throw new Error('static_game_artifact_primary_surface_missing');
4757
+ }
4758
+ if (!agentWorkspaceDir) {
4759
+ throw new Error('static_game_artifact_workspace_missing');
4760
+ }
4761
+ await client.workerCreateAgentTaskEvent(task.id, {
4762
+ workerKey,
4763
+ leaseToken,
4764
+ kind: 'system',
4765
+ phase: 'static_game_publish_started',
4766
+ message: 'Publishing the clean-room static game output.',
4767
+ });
4768
+ const taskClient = (0, apiClient_1.createCliApiClient)({
4769
+ baseUrl: envConfig.apiBase,
4770
+ token,
4771
+ onBehalfCreatorUsername: taskContext.creatorUsername,
4772
+ agentTaskToken: assignment.task.token,
4773
+ agentTaskId: task.id,
4774
+ agentTaskAttempt: taskContext.attempt,
4775
+ });
4776
+ const published = await (0, upload_1.publishStaticHtmlProject)({
4777
+ client: taskClient,
4778
+ taskId: task.id,
4779
+ taskToken: assignment.task.token,
4780
+ projectDir: agentWorkspaceDir,
4781
+ creatorUsername: taskContext.creatorUsername,
4782
+ primarySurface,
4783
+ apiBase: envConfig.apiBase,
4784
+ webBase: envConfig.webBase ?? null,
4785
+ claimedAppName: refreshed.task.claimedAppName,
4786
+ devRouterPort: devPort,
4787
+ token,
4788
+ user: me.user,
4789
+ });
4790
+ appId = published.appId;
4791
+ appVersionId = published.appVersionId;
4792
+ result = {
4793
+ appId,
4794
+ appVersionId,
4795
+ appName: published.appName,
4796
+ version: published.version,
4797
+ versionNodeId: published.versionNodeId,
4798
+ creatorUsername: published.creatorUsername,
4799
+ sourceHash: published.sourceHash,
4800
+ runtimeHash: published.runtimeHash,
4801
+ playUrl: published.playUrl,
4802
+ sourceContainsInjectedSdk: published.sourceContainsInjectedSdk,
4803
+ runtimeContainsInjectedSdk: published.runtimeContainsInjectedSdk,
4804
+ hostedLoadCheckPassed: published.hostedLoadCheckPassed,
4805
+ versionVisibility: 'PRIVATE',
4806
+ warnings: published.warnings,
4807
+ completedBy: 'worker_static_game_publish',
4808
+ };
4809
+ }
4810
+ if (!Number.isInteger(appId) || Number(appId) <= 0 || !Number.isInteger(appVersionId) || Number(appVersionId) <= 0) {
4811
+ throw new Error('static_game_publish_linked_version_missing');
4812
+ }
4813
+ await client.workerCompleteAgentTask(task.id, {
4814
+ workerKey,
4815
+ leaseToken,
4816
+ appId: Number(appId),
4817
+ appVersionId: Number(appVersionId),
4818
+ result,
4819
+ summary: 'Created and privately hosted a standalone static web game.',
4820
+ tokensUsed: agentResult.tokensUsed,
4821
+ });
4822
+ retainWorkspace = false;
4823
+ console.log(`Agent task ${task.id} published and completed as STATIC_GAME.`);
4824
+ }
4825
+ catch (error) {
4826
+ const rawMessage = error instanceof Error ? error.message : String(error);
4827
+ const artifactFailure = rawMessage.startsWith('static_game_artifact_');
4828
+ const failureCode = artifactFailure
4829
+ ? rawMessage.split(':', 1)[0]
4830
+ : 'static_game_publish_failed';
4831
+ await client.workerCreateAgentTaskEvent(task.id, {
4832
+ workerKey,
4833
+ leaseToken,
4834
+ kind: 'system',
4835
+ phase: artifactFailure ? 'static_game_artifact_failed' : 'static_game_publish_failed',
4836
+ message: artifactFailure
4837
+ ? `Static game output was invalid: ${rawMessage}`
4838
+ : `Static game publishing failed: ${rawMessage}`,
4839
+ payload: { error: failureCode, detail: rawMessage },
4840
+ });
4841
+ retainWorkspace = true;
4842
+ const terminalReason = artifactFailure
4843
+ ? {
4844
+ kind: 'failure',
4845
+ category: 'upload_validation',
4846
+ code: failureCode,
4847
+ message: 'The static game output did not pass validation.',
4848
+ retryable: false,
4849
+ rawError: rawMessage,
4850
+ source: 'worker',
4851
+ }
4852
+ : {
4853
+ kind: 'failure',
4854
+ category: 'instrument_error',
4855
+ code: failureCode,
4856
+ message: 'The platform failed to publish the static game.',
4857
+ retryable: true,
4858
+ rawError: rawMessage,
4859
+ source: 'worker',
4860
+ };
4861
+ await failTaskWithRetry(client, task.id, {
4862
+ workerKey,
4863
+ leaseToken,
4864
+ error: failureCode,
4865
+ terminalReason,
4866
+ result: {
4867
+ stage: artifactFailure ? 'artifact' : 'publish',
4868
+ error: rawMessage,
4869
+ agentRunSucceeded: true,
4870
+ },
4871
+ });
4872
+ }
4873
+ }
4874
+ else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
4875
+ console.error(`Agent task ${task.id} uploaded an artifact but exited before task completion; failing it explicitly.`);
4876
+ await failUploadedArtifactWithoutTaskDone();
4366
4877
  }
4367
4878
  else {
4368
4879
  const tokenCap = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_TOKEN_CAP', runtime_1.DEFAULT_WORKER_TOKEN_CAP);
@@ -4716,6 +5227,9 @@ async function resolveTaskCommandContext(command, optionsEnv, taskContext) {
4716
5227
  }
4717
5228
  async function uploadTask(options = {}) {
4718
5229
  const taskContext = readTaskContextFile();
5230
+ if (taskContext.kind === 'STATIC_GAME') {
5231
+ throw new Error('task_upload_not_allowed_for_static_game');
5232
+ }
4719
5233
  if (taskContext.kind === 'GAME_REVIEW' || taskContext.kind === 'GAME_EVAL') {
4720
5234
  throw new Error('task_upload_not_allowed_for_game_review');
4721
5235
  }
@@ -4828,6 +5342,7 @@ async function completeTask(options) {
4828
5342
  if (nextSteps === NEXT_STEPS_READ_FAILED) {
4829
5343
  return;
4830
5344
  }
5345
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
4831
5346
  try {
4832
5347
  await ctx.client.workerCompleteAgentTask(taskContext.taskId, {
4833
5348
  taskToken: taskContext.taskToken,
@@ -4844,6 +5359,7 @@ async function completeTask(options) {
4844
5359
  },
4845
5360
  summary,
4846
5361
  ...(nextSteps !== undefined ? { nextSteps } : {}),
5362
+ ...(platformFeedback ? { platformFeedback } : {}),
4847
5363
  });
4848
5364
  }
4849
5365
  catch (error) {
@@ -4858,6 +5374,7 @@ async function completeTask(options) {
4858
5374
  appName: uploadResult.appName,
4859
5375
  version: uploadResult.version,
4860
5376
  },
5377
+ ...(platformFeedback ? { platformFeedback } : {}),
4861
5378
  });
4862
5379
  }
4863
5380
  throw error;
@@ -4903,6 +5420,27 @@ function readOptionalTextFile(filePath) {
4903
5420
  const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
4904
5421
  return content || null;
4905
5422
  }
5423
+ function readPlatformFeedbackFile(filePath) {
5424
+ const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
5425
+ if (!normalizedPath) {
5426
+ return null;
5427
+ }
5428
+ try {
5429
+ const resolved = node_path_1.default.resolve(normalizedPath);
5430
+ if (!(0, node_fs_1.existsSync)(resolved)) {
5431
+ throw new Error('file_missing');
5432
+ }
5433
+ const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
5434
+ if (!content || content.length > 10000) {
5435
+ throw new Error(content ? 'file_too_long' : 'file_empty');
5436
+ }
5437
+ return content;
5438
+ }
5439
+ catch (error) {
5440
+ console.warn(`Platform feedback was not attached: ${error instanceof Error ? error.message : String(error)}`);
5441
+ return null;
5442
+ }
5443
+ }
4906
5444
  function contentTypeForEvidenceFile(fileName) {
4907
5445
  const extension = node_path_1.default.extname(fileName).toLowerCase();
4908
5446
  if (extension === '.png')
@@ -4981,6 +5519,7 @@ async function submitReviewTask(options) {
4981
5519
  const creatorFeedback = state === 'ERROR'
4982
5520
  ? null
4983
5521
  : readOptionalTextFile(creatorFeedbackFile ?? undefined);
5522
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
4984
5523
  await (0, review_1.validateGameReviewResult)({
4985
5524
  reviewState: state,
4986
5525
  reviewMessage,
@@ -5009,6 +5548,7 @@ async function submitReviewTask(options) {
5009
5548
  reviewState: state,
5010
5549
  reviewMessage,
5011
5550
  creatorFeedback,
5551
+ ...(platformFeedback ? { platformFeedback } : {}),
5012
5552
  ...(nextSteps !== undefined ? { nextSteps } : {}),
5013
5553
  evidenceFiles,
5014
5554
  instrumentEvidence,
@@ -5136,11 +5676,13 @@ async function failTask(options) {
5136
5676
  if (!ctx) {
5137
5677
  return;
5138
5678
  }
5679
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
5139
5680
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
5140
5681
  taskToken: taskContext.taskToken,
5141
5682
  error: message,
5142
5683
  terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'agent'),
5143
5684
  result: { failedBy: 'agent' },
5685
+ ...(platformFeedback ? { platformFeedback } : {}),
5144
5686
  });
5145
5687
  (0, output_1.printSuccess)('Task marked failed. This was the final operation; send one short final status sentence and stop immediately.');
5146
5688
  }