@playdrop/playdrop-cli 0.12.31 → 0.12.33

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 (27) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apiClient.js +16 -1
  3. package/dist/commands/upload.js +2 -0
  4. package/dist/commands/worker/runtime.d.ts +2 -0
  5. package/dist/commands/worker/runtime.js +14 -10
  6. package/dist/commands/worker.d.ts +23 -30
  7. package/dist/commands/worker.js +639 -656
  8. package/dist/index.js +6 -0
  9. package/node_modules/@playdrop/api-client/dist/client.d.ts +2 -1
  10. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  11. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +2 -1
  12. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  13. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +11 -0
  14. package/node_modules/@playdrop/api-client/dist/index.d.ts +3 -2
  15. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  16. package/node_modules/@playdrop/api-client/dist/index.js +5 -0
  17. package/node_modules/@playdrop/config/client-meta.json +2 -2
  18. package/node_modules/@playdrop/types/dist/agent-task-terminal.d.ts +5 -0
  19. package/node_modules/@playdrop/types/dist/agent-task-terminal.d.ts.map +1 -0
  20. package/node_modules/@playdrop/types/dist/agent-task-terminal.js +117 -0
  21. package/node_modules/@playdrop/types/dist/api.d.ts +100 -102
  22. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  23. package/node_modules/@playdrop/types/dist/api.js +1 -1
  24. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  25. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  26. package/node_modules/@playdrop/types/dist/index.js +3 -1
  27. package/package.json +1 -1
@@ -7,7 +7,7 @@ exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALL
7
7
  exports.allocateWorkerDevPort = allocateWorkerDevPort;
8
8
  exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
9
9
  exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
10
- exports.resolveWorkerClaimTaskAssignment = resolveWorkerClaimTaskAssignment;
10
+ exports.normalizeWorkerTaskAssignmentV2 = normalizeWorkerTaskAssignmentV2;
11
11
  exports.nextClaimBackoffMs = nextClaimBackoffMs;
12
12
  exports.claimBackoffDelayMs = claimBackoffDelayMs;
13
13
  exports.resolveWorkerHomeDir = resolveWorkerHomeDir;
@@ -16,9 +16,7 @@ exports.hasAgentTaskUploadedArtifact = hasAgentTaskUploadedArtifact;
16
16
  exports.appendWorkerTranscriptChunks = appendWorkerTranscriptChunks;
17
17
  exports.readTaskNextStepsFile = readTaskNextStepsFile;
18
18
  exports.assertTaskUploadResultMatchesContext = assertTaskUploadResultMatchesContext;
19
- exports.stageAssignmentWorkspaceFiles = stageAssignmentWorkspaceFiles;
20
- exports.stagePlaydropPluginReferences = stagePlaydropPluginReferences;
21
- exports.stageCodexTaskSkills = stageCodexTaskSkills;
19
+ exports.stageAssignmentWorkspaceV2 = stageAssignmentWorkspaceV2;
22
20
  exports.assertAssignmentPluginBundleExtractionBounds = assertAssignmentPluginBundleExtractionBounds;
23
21
  exports.stageAssignmentPluginBundle = stageAssignmentPluginBundle;
24
22
  exports.resolveDevPluginWorkingTree = resolveDevPluginWorkingTree;
@@ -35,12 +33,11 @@ exports.resolvePlaywrightBrowsersPathFromExecutable = resolvePlaywrightBrowsersP
35
33
  exports.loadWorkerEnvFile = loadWorkerEnvFile;
36
34
  exports.buildWorkerCapabilities = buildWorkerCapabilities;
37
35
  exports.buildWorkerClaimBody = buildWorkerClaimBody;
38
- exports.resolveWorkerBaseSource = resolveWorkerBaseSource;
39
36
  exports.extractZipArchive = extractZipArchive;
40
- exports.readWorkerProjectVersion = readWorkerProjectVersion;
41
37
  exports.buildCataloguePreviewPayload = buildCataloguePreviewPayload;
42
- exports.assertWorkerProjectVersionBumped = assertWorkerProjectVersionBumped;
43
- exports.removeStaleWorkerControlFilesFromBaseSource = removeStaleWorkerControlFilesFromBaseSource;
38
+ exports.removeStaleWorkerControlFiles = removeStaleWorkerControlFiles;
39
+ exports.resolveCodexModel = resolveCodexModel;
40
+ exports.resolveClaudeModel = resolveClaudeModel;
44
41
  exports.prepareCodexRunHomes = prepareCodexRunHomes;
45
42
  exports.prepareClaudeRunConfig = prepareClaudeRunConfig;
46
43
  exports.removeAgentRunCredentials = removeAgentRunCredentials;
@@ -74,6 +71,7 @@ exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
74
71
  exports.enforceInstrumentBrowserHygiene = enforceInstrumentBrowserHygiene;
75
72
  exports.startWorker = startWorker;
76
73
  exports.reportTask = reportTask;
74
+ exports.showTaskContext = showTaskContext;
77
75
  exports.reportCatalogueTask = reportCatalogueTask;
78
76
  exports.uploadTask = uploadTask;
79
77
  exports.claimSlugTask = claimSlugTask;
@@ -95,6 +93,7 @@ const node_os_1 = __importDefault(require("node:os"));
95
93
  const node_path_1 = __importDefault(require("node:path"));
96
94
  const node_process_1 = __importDefault(require("node:process"));
97
95
  const node_util_1 = require("node:util");
96
+ const apiClient_1 = require("../apiClient");
98
97
  const clientInfo_1 = require("../clientInfo");
99
98
  const commandContext_1 = require("../commandContext");
100
99
  const config_1 = require("../config");
@@ -134,15 +133,13 @@ const CLAIM_BACKOFF_JITTER_MS = 500;
134
133
  const FAIL_REPORT_RETRY_DELAY_MS = 2000;
135
134
  const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
136
135
  const SLACK_API_BASE_URL = 'https://slack.com/api';
137
- const WORKER_WRAPPER_CONTRACT_VERSION = 1;
136
+ const WORKER_WRAPPER_CONTRACT_VERSION = 2;
138
137
  const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
139
- const PLAYDROP_PLUGIN_MANIFEST_REQUIRED_STARTUP_KINDS = WORKER_SUPPORTED_KINDS;
140
138
  const requireFromWorker = (0, node_module_1.createRequire)(__filename);
141
- const STAGED_PLAYDROP_PLUGIN_ROOT = '.playdrop/plugin';
142
- const STAGED_CODEX_SKILL_ROOT = '.agents/skills';
143
- const PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH = 'playdrop-worker-staging.json';
139
+ const AGENT_BUNDLE_MANIFEST_PATH = 'agent-bundle.json';
144
140
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES = 1000;
145
141
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_UNCOMPRESSED_BYTES = 20 * 1024 * 1024;
142
+ const ASSIGNMENT_WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
146
143
  const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
147
144
  const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
148
145
  const ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES = 22;
@@ -150,6 +147,7 @@ const ZIP_MAX_COMMENT_BYTES = 0xffff;
150
147
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
151
148
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
152
149
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
150
+ ['task', 'context'],
153
151
  ['task', 'report'],
154
152
  ['task', 'report-catalogue'],
155
153
  ['task', 'claim-slug'],
@@ -298,105 +296,162 @@ function normalizeAssignmentAgent(value) {
298
296
  }
299
297
  return value;
300
298
  }
301
- function normalizeAssignmentKind(value, task) {
302
- if (value !== 'NEW_GAME' && value !== 'REMIX_GAME' && value !== 'GAME_UPDATE' && value !== 'GAME_REVIEW' && value !== 'GAME_EVAL') {
303
- throw new Error('agent_task_assignment_kind_missing');
304
- }
305
- if (value !== task.kind) {
306
- throw new Error('agent_task_assignment_kind_mismatch');
307
- }
308
- return value;
309
- }
310
299
  function normalizeAssignmentTarget(value) {
311
300
  if (value !== 'FIRST_PARTY' && value !== 'PERSONAL') {
312
301
  throw new Error('agent_task_assignment_target_missing');
313
302
  }
314
303
  return value;
315
304
  }
316
- function resolveWorkerClaimTaskAssignment(claim) {
317
- if (!claim.task) {
318
- return null;
305
+ function normalizeAssignmentContent(input) {
306
+ const relativePath = normalizeAssignmentWorkspaceRelativePath(input.path);
307
+ const contentBase64 = typeof input.contentBase64 === 'string' ? input.contentBase64.trim() : '';
308
+ const sha256 = typeof input.sha256 === 'string' ? input.sha256.trim() : '';
309
+ const sizeBytes = Number(input.sizeBytes);
310
+ if (!contentBase64 || !/^[A-Za-z0-9+/]+={0,2}$/.test(contentBase64)) {
311
+ throw new Error(`${input.code}_content_invalid`);
319
312
  }
320
- const assignment = claim.taskAssignment;
321
- if (!assignment || typeof assignment !== 'object') {
322
- throw new Error('agent_task_claim_missing_task_assignment');
313
+ if (!/^[a-f0-9]{64}$/.test(sha256) || !Number.isInteger(sizeBytes) || sizeBytes <= 0) {
314
+ throw new Error(`${input.code}_metadata_invalid`);
323
315
  }
324
- const prompt = typeof assignment.prompt === 'string' ? assignment.prompt.trim() : '';
325
- if (!prompt) {
326
- throw new Error('agent_task_assignment_prompt_missing');
327
- }
328
- const taskId = parsePositiveInteger(assignment.taskId, 'agent_task_assignment_task_id_missing');
329
- if (taskId !== claim.task.id) {
330
- throw new Error('agent_task_assignment_task_id_mismatch');
316
+ const buffer = Buffer.from(contentBase64, 'base64');
317
+ if (buffer.length !== sizeBytes) {
318
+ throw new Error(`${input.code}_size_mismatch`);
331
319
  }
332
- const taskToken = typeof assignment.taskToken === 'string' ? assignment.taskToken.trim() : '';
333
- if (!taskToken) {
334
- throw new Error('agent_task_assignment_token_missing');
320
+ if ((0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex') !== sha256) {
321
+ throw new Error(`${input.code}_sha_mismatch`);
335
322
  }
336
- const model = typeof assignment.model === 'string' ? assignment.model.trim() : '';
337
- if (!model) {
338
- throw new Error('agent_task_assignment_model_missing');
339
- }
340
- const workspace = assignment.workspace && typeof assignment.workspace === 'object' && !Array.isArray(assignment.workspace)
341
- ? assignment.workspace
342
- : null;
343
- if (!workspace) {
344
- throw new Error('agent_task_assignment_workspace_missing');
323
+ return { path: relativePath, contentBase64, sha256, sizeBytes, buffer };
324
+ }
325
+ function normalizeWorkerTaskAssignmentV2(claim) {
326
+ if (claim.protocolVersion !== 2) {
327
+ throw new Error('agent_task_claim_protocol_version_mismatch');
345
328
  }
346
- const inputs = assignment.inputs && typeof assignment.inputs === 'object' && !Array.isArray(assignment.inputs)
347
- ? assignment.inputs
348
- : null;
349
- if (!inputs) {
350
- throw new Error('agent_task_assignment_inputs_missing');
329
+ if (claim.action !== 'run') {
330
+ return null;
351
331
  }
352
- const output = assignment.output && typeof assignment.output === 'object' && !Array.isArray(assignment.output)
353
- ? assignment.output
354
- : null;
355
- if (!output) {
356
- throw new Error('agent_task_assignment_output_missing');
332
+ const assignment = claim.assignment;
333
+ const taskId = parsePositiveInteger(assignment.task.id, 'agent_task_assignment_task_id_missing');
334
+ const taskToken = typeof assignment.task.token === 'string' ? assignment.task.token.trim() : '';
335
+ const taskType = typeof assignment.task.type === 'string' ? assignment.task.type.trim() : '';
336
+ const attempt = parsePositiveInteger(assignment.task.attempt, 'agent_task_assignment_attempt_missing');
337
+ const prompt = typeof assignment.request.prompt === 'string' ? assignment.request.prompt : '';
338
+ const model = typeof assignment.agent.model === 'string' ? assignment.agent.model.trim() : '';
339
+ if (!taskToken || !taskType || !prompt.trim() || !model) {
340
+ throw new Error('agent_task_assignment_invalid');
341
+ }
342
+ const workspacePaths = new Set();
343
+ const files = assignment.workspace.files.map((file) => {
344
+ const normalized = normalizeAssignmentContent({
345
+ path: file.path,
346
+ contentBase64: file.contentBase64,
347
+ sha256: file.sha256,
348
+ sizeBytes: file.sizeBytes,
349
+ code: 'agent_task_assignment_workspace_file',
350
+ });
351
+ const contentType = typeof file.contentType === 'string' ? file.contentType.trim() : '';
352
+ if (!contentType) {
353
+ throw new Error('agent_task_assignment_workspace_file_content_type_missing');
354
+ }
355
+ if (workspacePaths.has(normalized.path)) {
356
+ throw new Error('agent_task_assignment_workspace_path_duplicate');
357
+ }
358
+ workspacePaths.add(normalized.path);
359
+ return {
360
+ path: normalized.path,
361
+ sha256: normalized.sha256,
362
+ sizeBytes: normalized.sizeBytes,
363
+ contentType,
364
+ contentBase64: normalized.contentBase64,
365
+ };
366
+ });
367
+ const archives = assignment.workspace.archives.map((archive) => {
368
+ if (archive.format !== 'zip') {
369
+ throw new Error('agent_task_assignment_workspace_archive_format_invalid');
370
+ }
371
+ const normalized = normalizeAssignmentContent({
372
+ path: archive.path,
373
+ contentBase64: archive.contentBase64,
374
+ sha256: archive.sha256,
375
+ sizeBytes: archive.sizeBytes,
376
+ code: 'agent_task_assignment_workspace_archive',
377
+ });
378
+ if (workspacePaths.has(normalized.path)) {
379
+ throw new Error('agent_task_assignment_workspace_path_duplicate');
380
+ }
381
+ workspacePaths.add(normalized.path);
382
+ return {
383
+ path: normalized.path,
384
+ format: 'zip',
385
+ sha256: normalized.sha256,
386
+ sizeBytes: normalized.sizeBytes,
387
+ contentBase64: normalized.contentBase64,
388
+ };
389
+ });
390
+ const fileByPath = new Map(files.map((file) => [file.path, file]));
391
+ const attachmentIds = new Set();
392
+ const attachments = assignment.request.attachments.map((attachment) => {
393
+ const id = typeof attachment.id === 'string' ? attachment.id.trim() : '';
394
+ const attachmentPath = normalizeAssignmentWorkspaceRelativePath(attachment.path);
395
+ const fileName = typeof attachment.fileName === 'string' ? attachment.fileName.trim() : '';
396
+ const contentType = typeof attachment.contentType === 'string' ? attachment.contentType.trim() : '';
397
+ const sha256 = typeof attachment.sha256 === 'string' ? attachment.sha256.trim() : '';
398
+ const sizeBytes = Number(attachment.sizeBytes);
399
+ const file = fileByPath.get(attachmentPath);
400
+ if (!id || attachmentIds.has(id) || !fileName || !contentType || !file) {
401
+ throw new Error('agent_task_assignment_attachment_invalid');
402
+ }
403
+ if (sha256 !== file.sha256 || sizeBytes !== file.sizeBytes || contentType !== file.contentType) {
404
+ throw new Error('agent_task_assignment_attachment_metadata_mismatch');
405
+ }
406
+ attachmentIds.add(id);
407
+ return {
408
+ id,
409
+ path: attachmentPath,
410
+ fileName,
411
+ contentType,
412
+ sizeBytes,
413
+ sha256,
414
+ description: typeof attachment.description === 'string' ? attachment.description : null,
415
+ };
416
+ });
417
+ for (const attachment of attachments) {
418
+ if (!fileByPath.has(attachment.path)) {
419
+ throw new Error('agent_task_assignment_attachment_file_missing');
420
+ }
357
421
  }
358
- const version = typeof output.version === 'string' ? output.version.trim() : '';
359
- if (!version) {
360
- throw new Error('agent_task_assignment_output_version_missing');
422
+ const browser = assignment.agent.browser;
423
+ if (browser !== 'NONE' && browser !== 'CHROME' && browser !== 'PLAYWRIGHT') {
424
+ throw new Error('agent_task_assignment_browser_invalid');
361
425
  }
362
- const pluginBundle = assignment.pluginBundle && typeof assignment.pluginBundle === 'object' && !Array.isArray(assignment.pluginBundle)
363
- ? assignment.pluginBundle
364
- : null;
365
- if (!pluginBundle) {
366
- throw new Error('agent_task_assignment_plugin_bundle_missing');
426
+ const reasoningEffort = assignment.agent.reasoningEffort;
427
+ if (reasoningEffort !== null && typeof reasoningEffort !== 'string') {
428
+ throw new Error('agent_task_assignment_reasoning_effort_invalid');
367
429
  }
368
430
  return {
369
- taskId,
370
- taskToken,
371
- kind: normalizeAssignmentKind(assignment.kind, claim.task),
372
- target: normalizeAssignmentTarget(assignment.target),
373
- agent: normalizeAssignmentAgent(assignment.agent),
374
- model,
431
+ task: {
432
+ id: taskId,
433
+ token: taskToken,
434
+ type: taskType,
435
+ executionTarget: normalizeAssignmentTarget(assignment.task.executionTarget),
436
+ attempt,
437
+ metadata: assignment.task.metadata,
438
+ },
375
439
  workspace: {
376
- folderName: normalizeAssignmentWorkspaceFolder(workspace.folderName),
377
- files: normalizeAssignmentWorkspaceFiles(workspace.files),
440
+ folderName: normalizeAssignmentWorkspaceFolder(assignment.workspace.folderName),
441
+ archives,
442
+ files,
378
443
  },
379
- inputs: {
380
- request: {
381
- prompt: normalizeAssignmentRequest(inputs.request),
382
- },
383
- attachments: Array.isArray(inputs.attachments) ? inputs.attachments : [],
384
- baseSource: inputs.baseSource ?? null,
385
- remixSource: inputs.remixSource ?? null,
386
- reviewTarget: inputs.reviewTarget ?? null,
387
- evalTarget: inputs.evalTarget ?? null,
388
- allowedTemplateKeys: Array.isArray(inputs.allowedTemplateKeys)
389
- ? Array.from(new Set(inputs.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
390
- : [],
444
+ plugin: assignment.plugin,
445
+ request: {
446
+ prompt,
447
+ attachments,
391
448
  },
392
- pluginBundle,
393
- output: {
394
- appId: typeof output.appId === 'number' && Number.isInteger(output.appId) && output.appId > 0 ? output.appId : null,
395
- appName: typeof output.appName === 'string' && output.appName.trim() ? output.appName.trim() : null,
396
- version,
397
- releaseNoteRequired: output.releaseNoteRequired !== false,
449
+ agent: {
450
+ runtime: normalizeAssignmentAgent(assignment.agent.runtime),
451
+ model,
452
+ reasoningEffort,
453
+ browser,
398
454
  },
399
- prompt,
400
455
  };
401
456
  }
402
457
  function nextClaimBackoffMs(currentMs) {
@@ -445,73 +500,55 @@ async function pruneWorkerTaskWorkspaces(incomingWorkspaceDir, retention = expor
445
500
  }
446
501
  return removed;
447
502
  }
448
- function buildWorkerTaskContext(claim, assignment) {
449
- const task = claim.task;
450
- if (!task) {
451
- throw new Error('agent_task_claim_missing_task');
452
- }
453
- if (task.id !== assignment.taskId) {
454
- throw new Error('agent_task_context_task_id_mismatch');
503
+ function buildWorkerTaskContext(input) {
504
+ const playdrop = input.metadata.playdrop;
505
+ if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
506
+ throw new Error('agent_task_context_playdrop_metadata_missing');
455
507
  }
456
- if (task.kind !== assignment.kind) {
457
- throw new Error('agent_task_context_kind_mismatch');
508
+ const metadata = playdrop;
509
+ const kind = metadata.kind;
510
+ if (kind !== 'NEW_GAME' && kind !== 'REMIX_GAME' && kind !== 'GAME_UPDATE' && kind !== 'GAME_REVIEW' && kind !== 'GAME_EVAL') {
511
+ throw new Error('agent_task_context_kind_missing');
458
512
  }
459
- if (task.executionTarget !== assignment.target) {
460
- throw new Error('agent_task_context_target_mismatch');
461
- }
462
- const creatorUsername = typeof task.creatorUsername === 'string' ? task.creatorUsername.trim() : '';
463
- const outputVersion = typeof assignment.output.version === 'string' ? assignment.output.version.trim() : '';
513
+ const creatorUsername = typeof metadata.creatorUsername === 'string'
514
+ ? metadata.creatorUsername.trim()
515
+ : '';
516
+ const output = metadata.output && typeof metadata.output === 'object' && !Array.isArray(metadata.output)
517
+ ? metadata.output
518
+ : null;
519
+ const outputVersion = typeof output?.version === 'string' ? output.version.trim() : '';
520
+ const outputAppName = typeof output?.appName === 'string' && output.appName.trim()
521
+ ? output.appName.trim()
522
+ : null;
464
523
  if (!creatorUsername || !outputVersion) {
465
524
  throw new Error('agent_task_context_invalid');
466
525
  }
467
- const taskToken = typeof assignment.taskToken === 'string' ? assignment.taskToken.trim() : '';
468
- if (!taskToken) {
469
- throw new Error('agent_task_context_token_missing');
470
- }
471
- const attempt = Number(task.attempts);
472
- if (!Number.isInteger(attempt) || attempt <= 0) {
473
- throw new Error('agent_task_context_attempt_missing');
474
- }
475
- const outputAppName = typeof assignment.output.appName === 'string' && assignment.output.appName.trim()
476
- ? assignment.output.appName.trim()
526
+ const remixSource = metadata.remixSource && typeof metadata.remixSource === 'object' && !Array.isArray(metadata.remixSource)
527
+ ? metadata.remixSource
477
528
  : null;
478
- const reviewTarget = assignment.inputs.reviewTarget && typeof assignment.inputs.reviewTarget === 'object'
479
- ? assignment.inputs.reviewTarget
529
+ const remixSourceRef = typeof remixSource?.ref === 'string' && remixSource.ref.trim()
530
+ ? remixSource.ref.trim()
480
531
  : null;
481
- let reviewAppVersionId = null;
482
- if (assignment.kind === 'GAME_REVIEW') {
483
- const parsedReviewAppVersionId = Number(reviewTarget?.appVersionId);
484
- if (!Number.isInteger(parsedReviewAppVersionId) || parsedReviewAppVersionId <= 0) {
485
- throw new Error('agent_task_context_review_target_missing');
486
- }
487
- reviewAppVersionId = parsedReviewAppVersionId;
488
- }
489
- const remixSource = assignment.inputs.remixSource && typeof assignment.inputs.remixSource === 'object'
490
- ? assignment.inputs.remixSource
532
+ const reviewTarget = metadata.reviewTarget && typeof metadata.reviewTarget === 'object' && !Array.isArray(metadata.reviewTarget)
533
+ ? metadata.reviewTarget
491
534
  : null;
492
- let remixSourceRef = null;
493
- if (assignment.kind === 'REMIX_GAME') {
494
- const parsedRemixSourceRef = typeof remixSource?.ref === 'string' ? remixSource.ref.trim() : '';
495
- if (!parsedRemixSourceRef) {
496
- throw new Error('agent_task_context_remix_source_missing');
497
- }
498
- remixSourceRef = parsedRemixSourceRef;
499
- }
500
- const allowedTemplateKeys = Array.isArray(assignment.inputs.allowedTemplateKeys)
501
- ? Array.from(new Set(assignment.inputs.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
535
+ const rawReviewAppVersionId = Number(reviewTarget?.appVersionId);
536
+ const reviewAppVersionId = Number.isInteger(rawReviewAppVersionId) && rawReviewAppVersionId > 0
537
+ ? rawReviewAppVersionId
538
+ : null;
539
+ const allowedTemplateKeys = Array.isArray(metadata.allowedTemplateKeys)
540
+ ? metadata.allowedTemplateKeys.filter((value) => typeof value === 'string' && Boolean(value.trim()))
502
541
  : [];
503
- const creatorRequest = assignment.inputs.request.prompt.trim();
504
- if (!creatorRequest) {
505
- throw new Error('agent_task_context_creator_request_missing');
506
- }
507
542
  return {
508
- taskId: task.id,
509
- attempt,
510
- taskToken,
511
- kind: assignment.kind,
543
+ taskId: input.taskId,
544
+ attempt: input.attempt,
545
+ taskToken: input.taskToken,
546
+ taskType: input.taskType,
547
+ metadata: input.metadata,
548
+ kind,
512
549
  creatorUsername,
513
- creatorRequest,
514
- target: assignment.target,
550
+ creatorRequest: input.prompt,
551
+ target: input.target,
515
552
  outputAppName,
516
553
  outputVersion,
517
554
  remixSourceRef,
@@ -519,12 +556,62 @@ function buildWorkerTaskContext(claim, assignment) {
519
556
  reviewAppVersionId,
520
557
  };
521
558
  }
559
+ function buildWorkerTaskContextV2(assignment) {
560
+ return buildWorkerTaskContext({
561
+ taskId: assignment.task.id,
562
+ attempt: assignment.task.attempt,
563
+ taskToken: assignment.task.token,
564
+ taskType: assignment.task.type,
565
+ metadata: assignment.task.metadata,
566
+ prompt: assignment.request.prompt,
567
+ target: assignment.task.executionTarget,
568
+ });
569
+ }
522
570
  function workerTaskContextPath(workspaceDir) {
523
571
  return node_path_1.default.join(workspaceDir, '.playdrop', 'task.json');
524
572
  }
525
573
  function workerTaskUploadResultPath(workspaceDir) {
526
574
  return node_path_1.default.join(workspaceDir, '.playdrop', 'task-upload-result.json');
527
575
  }
576
+ function serializeWorkerTaskContext(taskContext) {
577
+ const playdrop = taskContext.metadata.playdrop;
578
+ if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
579
+ throw new Error('agent_task_context_playdrop_metadata_missing');
580
+ }
581
+ const playdropMetadata = playdrop;
582
+ const output = playdropMetadata.output;
583
+ if (!output || typeof output !== 'object' || Array.isArray(output)) {
584
+ throw new Error('agent_task_context_invalid');
585
+ }
586
+ const metadata = {
587
+ ...taskContext.metadata,
588
+ playdrop: {
589
+ ...playdropMetadata,
590
+ output: {
591
+ ...output,
592
+ appName: taskContext.outputAppName,
593
+ },
594
+ },
595
+ };
596
+ return {
597
+ protocolVersion: 2,
598
+ task: {
599
+ id: taskContext.taskId,
600
+ attempt: taskContext.attempt,
601
+ token: taskContext.taskToken,
602
+ type: taskContext.taskType,
603
+ executionTarget: taskContext.target,
604
+ metadata,
605
+ },
606
+ request: {
607
+ prompt: taskContext.creatorRequest,
608
+ },
609
+ worker: {
610
+ devPort: taskContext.devPort,
611
+ env: taskContext.env,
612
+ },
613
+ };
614
+ }
528
615
  async function stageWorkerTaskContext(input) {
529
616
  const playdropDir = node_path_1.default.join(input.workspaceDir, '.playdrop');
530
617
  await (0, promises_1.mkdir)(playdropDir, { recursive: true });
@@ -533,18 +620,10 @@ async function stageWorkerTaskContext(input) {
533
620
  devPort: input.devPort,
534
621
  env: input.env,
535
622
  };
536
- await (0, promises_1.writeFile)(workerTaskContextPath(input.workspaceDir), JSON.stringify(taskFile, null, 2), 'utf8');
537
- await (0, promises_1.writeFile)(node_path_1.default.join(input.workspaceDir, '.playdrop.json'), JSON.stringify({
538
- ownerUsername: input.taskContext.creatorUsername,
539
- env: input.env,
540
- taskId: input.taskContext.taskId,
541
- taskAttempt: input.taskContext.attempt,
542
- devPort: input.devPort,
543
- taskToken: input.taskContext.taskToken,
544
- }, null, 2), 'utf8');
623
+ await (0, promises_1.writeFile)(workerTaskContextPath(input.workspaceDir), JSON.stringify(serializeWorkerTaskContext(taskFile), null, 2), 'utf8');
545
624
  }
546
625
  async function writeWorkerTaskContextFile(workspaceDir, taskContext) {
547
- await (0, promises_1.writeFile)(workerTaskContextPath(workspaceDir), JSON.stringify(taskContext, null, 2), 'utf8');
626
+ await (0, promises_1.writeFile)(workerTaskContextPath(workspaceDir), JSON.stringify(serializeWorkerTaskContext(taskContext), null, 2), 'utf8');
548
627
  }
549
628
  function findWorkspaceTaskFile(startDir, basename) {
550
629
  let current = node_path_1.default.resolve(startDir);
@@ -563,67 +642,57 @@ function findWorkspaceTaskFile(startDir, basename) {
563
642
  function readTaskContextFile(startDir = node_process_1.default.cwd()) {
564
643
  const file = findWorkspaceTaskFile(startDir, 'task.json');
565
644
  const parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
566
- const taskId = Number(parsed.taskId);
567
- const attempt = Number(parsed.attempt);
568
- const taskToken = typeof parsed.taskToken === 'string' ? parsed.taskToken.trim() : '';
569
- const creatorUsername = typeof parsed.creatorUsername === 'string' ? parsed.creatorUsername.trim() : '';
570
- const env = typeof parsed.env === 'string' ? parsed.env.trim() : '';
571
- const creatorRequest = typeof parsed.creatorRequest === 'string' ? parsed.creatorRequest.trim() : '';
572
- const outputVersion = typeof parsed.outputVersion === 'string' ? parsed.outputVersion.trim() : '';
573
- const remixSourceRef = typeof parsed.remixSourceRef === 'string' && parsed.remixSourceRef.trim()
574
- ? parsed.remixSourceRef.trim()
645
+ const task = parsed.task && typeof parsed.task === 'object' && !Array.isArray(parsed.task)
646
+ ? parsed.task
575
647
  : null;
576
- const allowedTemplateKeys = Array.isArray(parsed.allowedTemplateKeys)
577
- ? Array.from(new Set(parsed.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
578
- : [];
579
- let reviewAppVersionId = null;
580
- if (parsed.reviewAppVersionId !== null && parsed.reviewAppVersionId !== undefined) {
581
- const parsedReviewAppVersionId = Number(parsed.reviewAppVersionId);
582
- if (Number.isInteger(parsedReviewAppVersionId) && parsedReviewAppVersionId > 0) {
583
- reviewAppVersionId = parsedReviewAppVersionId;
584
- }
585
- }
586
- const devPort = typeof parsed.devPort === 'number' ? parsed.devPort : Number.parseInt(String(parsed.devPort ?? ''), 10);
587
- if (!Number.isInteger(taskId) || taskId <= 0
648
+ const request = parsed.request && typeof parsed.request === 'object' && !Array.isArray(parsed.request)
649
+ ? parsed.request
650
+ : null;
651
+ const worker = parsed.worker && typeof parsed.worker === 'object' && !Array.isArray(parsed.worker)
652
+ ? parsed.worker
653
+ : null;
654
+ const taskId = Number(task?.id);
655
+ const attempt = Number(task?.attempt);
656
+ const taskToken = typeof task?.token === 'string' ? task.token.trim() : '';
657
+ const taskType = typeof task?.type === 'string' ? task.type.trim() : '';
658
+ const metadata = task?.metadata && typeof task.metadata === 'object' && !Array.isArray(task.metadata)
659
+ ? task.metadata
660
+ : null;
661
+ const prompt = typeof request?.prompt === 'string' ? request.prompt : '';
662
+ const env = typeof worker?.env === 'string' ? worker.env.trim() : '';
663
+ const devPort = typeof worker?.devPort === 'number'
664
+ ? worker.devPort
665
+ : Number.parseInt(String(worker?.devPort ?? ''), 10);
666
+ if (parsed.protocolVersion !== 2
667
+ || !Number.isInteger(taskId) || taskId <= 0
588
668
  || !Number.isInteger(attempt) || attempt <= 0
589
669
  || !taskToken
590
- || (parsed.kind !== 'NEW_GAME' && parsed.kind !== 'REMIX_GAME' && parsed.kind !== 'GAME_UPDATE' && parsed.kind !== 'GAME_REVIEW' && parsed.kind !== 'GAME_EVAL')
591
- || (parsed.target !== 'FIRST_PARTY' && parsed.target !== 'PERSONAL')
592
- || !creatorUsername
593
- || !creatorRequest
670
+ || !taskType
671
+ || !metadata
672
+ || (task?.executionTarget !== 'FIRST_PARTY' && task?.executionTarget !== 'PERSONAL')
673
+ || !prompt.trim()
594
674
  || !env
595
- || !outputVersion
596
- || (parsed.kind === 'REMIX_GAME' && !remixSourceRef)
597
- || (parsed.kind === 'GAME_REVIEW' && reviewAppVersionId === null)
598
675
  || !Number.isInteger(devPort) || devPort <= 0 || devPort > 65535) {
599
676
  throw new Error(`task_context_invalid:${file}`);
600
677
  }
601
- return {
602
- taskId,
603
- attempt,
604
- taskToken,
605
- kind: parsed.kind,
606
- creatorUsername,
607
- creatorRequest,
608
- target: parsed.target,
609
- outputAppName: typeof parsed.outputAppName === 'string' && parsed.outputAppName.trim() ? parsed.outputAppName.trim() : null,
610
- outputVersion,
611
- remixSourceRef,
612
- allowedTemplateKeys,
613
- reviewAppVersionId,
614
- devPort,
615
- env,
616
- };
617
- }
618
- function normalizeAssignmentRequest(value) {
619
- const request = value && typeof value === 'object' && !Array.isArray(value)
620
- ? value
621
- : null;
622
- const prompt = typeof request?.prompt === 'string' ? request.prompt.trim() : '';
623
- if (!prompt) {
624
- throw new Error('agent_task_assignment_request_prompt_missing');
678
+ try {
679
+ return {
680
+ ...buildWorkerTaskContext({
681
+ taskId,
682
+ attempt,
683
+ taskToken,
684
+ taskType,
685
+ metadata,
686
+ prompt,
687
+ target: task.executionTarget,
688
+ }),
689
+ devPort,
690
+ env,
691
+ };
692
+ }
693
+ catch {
694
+ throw new Error(`task_context_invalid:${file}`);
625
695
  }
626
- return prompt;
627
696
  }
628
697
  function hasAgentTaskUploadedArtifact(task) {
629
698
  return ((Number.isInteger(task.appId) && Number(task.appId) > 0)
@@ -816,41 +885,6 @@ function normalizeAssignmentWorkspaceRelativePath(value) {
816
885
  }
817
886
  return normalizedPath;
818
887
  }
819
- function normalizeAssignmentWorkspaceFile(value) {
820
- const file = value && typeof value === 'object' && !Array.isArray(value)
821
- ? value
822
- : null;
823
- if (!file) {
824
- throw new Error('agent_task_assignment_workspace_file_invalid');
825
- }
826
- const relativePath = normalizeAssignmentWorkspaceRelativePath(file.relativePath);
827
- const contentBase64 = typeof file.contentBase64 === 'string' ? file.contentBase64.trim() : '';
828
- if (!contentBase64 || contentBase64.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(contentBase64)) {
829
- throw new Error('agent_task_assignment_workspace_file_content_invalid');
830
- }
831
- const contentType = typeof file.contentType === 'string' && file.contentType.trim()
832
- ? file.contentType.trim()
833
- : undefined;
834
- return {
835
- relativePath,
836
- contentBase64,
837
- ...(contentType ? { contentType } : {}),
838
- };
839
- }
840
- function normalizeAssignmentWorkspaceFiles(value) {
841
- if (!Array.isArray(value)) {
842
- return [];
843
- }
844
- const seen = new Set();
845
- return value.map((entry) => {
846
- const normalized = normalizeAssignmentWorkspaceFile(entry);
847
- if (seen.has(normalized.relativePath)) {
848
- throw new Error('agent_task_assignment_workspace_file_duplicate');
849
- }
850
- seen.add(normalized.relativePath);
851
- return normalized;
852
- });
853
- }
854
888
  function resolveWorkspaceFileDestination(workspaceDir, relativePath) {
855
889
  const root = node_path_1.default.resolve(workspaceDir);
856
890
  const destination = node_path_1.default.resolve(root, ...relativePath.split('/'));
@@ -860,12 +894,31 @@ function resolveWorkspaceFileDestination(workspaceDir, relativePath) {
860
894
  }
861
895
  return destination;
862
896
  }
863
- async function stageAssignmentWorkspaceFiles(workspaceDir, files) {
864
- const normalizedFiles = normalizeAssignmentWorkspaceFiles(files);
865
- for (const file of normalizedFiles) {
866
- const destination = resolveWorkspaceFileDestination(workspaceDir, file.relativePath);
897
+ async function stageAssignmentWorkspaceV2(workspaceDir, workspace) {
898
+ for (const file of workspace.files) {
899
+ const normalized = normalizeAssignmentContent({
900
+ path: file.path,
901
+ contentBase64: file.contentBase64,
902
+ sha256: file.sha256,
903
+ sizeBytes: file.sizeBytes,
904
+ code: 'agent_task_assignment_workspace_file',
905
+ });
906
+ const destination = resolveWorkspaceFileDestination(workspaceDir, normalized.path);
867
907
  await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
868
- await (0, promises_1.writeFile)(destination, Buffer.from(file.contentBase64, 'base64'));
908
+ await (0, promises_1.writeFile)(destination, normalized.buffer);
909
+ }
910
+ for (const archive of workspace.archives) {
911
+ const normalized = normalizeAssignmentContent({
912
+ path: archive.path,
913
+ contentBase64: archive.contentBase64,
914
+ sha256: archive.sha256,
915
+ sizeBytes: archive.sizeBytes,
916
+ code: 'agent_task_assignment_workspace_archive',
917
+ });
918
+ const destination = resolveWorkspaceFileDestination(workspaceDir, normalized.path);
919
+ await (0, promises_1.mkdir)(destination, { recursive: true });
920
+ extractZipArchive(new Uint8Array(normalized.buffer), destination);
921
+ await removeStaleWorkerControlFiles(destination);
869
922
  }
870
923
  }
871
924
  function isFile(candidatePath) {
@@ -876,146 +929,6 @@ function isFile(candidatePath) {
876
929
  return false;
877
930
  }
878
931
  }
879
- function normalizePlaydropPluginManifestPath(input) {
880
- if (typeof input.value !== 'string' || input.value.trim().length <= 0) {
881
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: file paths must be non-empty strings in ${input.manifestPath}`);
882
- }
883
- if (input.value.includes('\0') || input.value.includes('\\')) {
884
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
885
- }
886
- if (node_path_1.default.isAbsolute(input.value) || /^[A-Za-z]:[\\/]/.test(input.value)) {
887
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
888
- }
889
- const normalized = node_path_1.default.posix.normalize(input.value);
890
- if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
891
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
892
- }
893
- return normalized;
894
- }
895
- function readPlaydropPluginStagingManifest(pluginRoot) {
896
- const resolvedRoot = node_path_1.default.resolve(pluginRoot);
897
- const manifestPath = node_path_1.default.join(resolvedRoot, PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH);
898
- if (!isFile(manifestPath)) {
899
- throw new Error(`playdrop_plugin_staging_manifest_missing: expected ${PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH} under ${resolvedRoot}`);
900
- }
901
- let parsed;
902
- try {
903
- parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
904
- }
905
- catch (error) {
906
- throw new Error(`playdrop_plugin_staging_manifest_invalid_json:${manifestPath}:${error instanceof Error ? error.message : String(error)}`);
907
- }
908
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
909
- throw new Error(`playdrop_plugin_staging_manifest_invalid:${manifestPath}`);
910
- }
911
- const rawManifest = parsed;
912
- if (rawManifest.version !== 1) {
913
- throw new Error(`playdrop_plugin_staging_manifest_unsupported_version:${manifestPath}`);
914
- }
915
- if (!rawManifest.taskFiles || typeof rawManifest.taskFiles !== 'object' || Array.isArray(rawManifest.taskFiles)) {
916
- throw new Error(`playdrop_plugin_staging_manifest_incomplete: taskFiles missing in ${manifestPath}`);
917
- }
918
- const taskFiles = {};
919
- const rawTaskFiles = rawManifest.taskFiles;
920
- for (const kind of PLAYDROP_PLUGIN_MANIFEST_REQUIRED_STARTUP_KINDS) {
921
- const rawFiles = rawTaskFiles[kind];
922
- if (!Array.isArray(rawFiles) || rawFiles.length <= 0) {
923
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${kind}: non-empty taskFiles entry required in ${manifestPath}`);
924
- }
925
- const seen = new Set();
926
- const files = rawFiles.map((value) => normalizePlaydropPluginManifestPath({ manifestPath, kind, value }));
927
- for (const relativePath of files) {
928
- if (seen.has(relativePath)) {
929
- throw new Error(`playdrop_plugin_staging_manifest_duplicate_file:${kind}:${relativePath}`);
930
- }
931
- seen.add(relativePath);
932
- const source = node_path_1.default.resolve(resolvedRoot, ...relativePath.split('/'));
933
- const relativeToRoot = node_path_1.default.relative(resolvedRoot, source);
934
- if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
935
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${kind}:${relativePath}`);
936
- }
937
- if (!isFile(source)) {
938
- throw new Error(`playdrop_plugin_staging_manifest_file_missing:${kind}:${relativePath}`);
939
- }
940
- }
941
- taskFiles[kind] = files;
942
- }
943
- for (const kind of WORKER_SUPPORTED_KINDS) {
944
- if (taskFiles[kind]) {
945
- continue;
946
- }
947
- const rawFiles = rawTaskFiles[kind];
948
- if (rawFiles === undefined) {
949
- continue;
950
- }
951
- if (!Array.isArray(rawFiles) || rawFiles.length <= 0) {
952
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${kind}: non-empty taskFiles entry required in ${manifestPath}`);
953
- }
954
- const seen = new Set();
955
- const files = rawFiles.map((value) => normalizePlaydropPluginManifestPath({ manifestPath, kind, value }));
956
- for (const relativePath of files) {
957
- if (seen.has(relativePath)) {
958
- throw new Error(`playdrop_plugin_staging_manifest_duplicate_file:${kind}:${relativePath}`);
959
- }
960
- seen.add(relativePath);
961
- const source = node_path_1.default.resolve(resolvedRoot, ...relativePath.split('/'));
962
- const relativeToRoot = node_path_1.default.relative(resolvedRoot, source);
963
- if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
964
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${kind}:${relativePath}`);
965
- }
966
- if (!isFile(source)) {
967
- throw new Error(`playdrop_plugin_staging_manifest_file_missing:${kind}:${relativePath}`);
968
- }
969
- }
970
- taskFiles[kind] = files;
971
- }
972
- return {
973
- version: 1,
974
- taskFiles: taskFiles,
975
- };
976
- }
977
- async function stagePlaydropPluginReferences(input) {
978
- const pluginRoot = node_path_1.default.resolve(input.pluginRoot);
979
- if (!WORKER_SUPPORTED_KINDS.includes(input.kind)) {
980
- throw new Error(`unsupported_agent_task_kind:${input.kind}`);
981
- }
982
- const manifest = readPlaydropPluginStagingManifest(pluginRoot);
983
- const referenceFiles = manifest.taskFiles[input.kind];
984
- if (!Array.isArray(referenceFiles) || referenceFiles.length <= 0) {
985
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: non-empty taskFiles entry required`);
986
- }
987
- const staged = [];
988
- for (const relativePath of referenceFiles) {
989
- const destinationRelativePath = node_path_1.default.join(STAGED_PLAYDROP_PLUGIN_ROOT, relativePath);
990
- const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
991
- await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
992
- await (0, promises_1.copyFile)(node_path_1.default.join(pluginRoot, relativePath), destination);
993
- staged.push(destinationRelativePath.replace(/\\/g, '/'));
994
- }
995
- return staged;
996
- }
997
- async function stageCodexTaskSkills(input) {
998
- const skillRoot = node_path_1.default.join(input.workspaceDir, STAGED_CODEX_SKILL_ROOT);
999
- await (0, promises_1.rm)(skillRoot, { recursive: true, force: true });
1000
- const stagedSkills = [];
1001
- for (const stagedPluginPath of input.stagedPluginPaths) {
1002
- const normalized = stagedPluginPath.replace(/\\/g, '/');
1003
- const match = /^\.playdrop\/plugin\/skills\/([^/]+)\/SKILL\.md$/.exec(normalized);
1004
- if (!match?.[1]) {
1005
- continue;
1006
- }
1007
- const source = resolveWorkspaceFileDestination(input.workspaceDir, normalized);
1008
- const destinationRelativePath = `${STAGED_CODEX_SKILL_ROOT}/${match[1]}/SKILL.md`;
1009
- const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
1010
- await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1011
- await (0, promises_1.copyFile)(source, destination);
1012
- stagedSkills.push(destinationRelativePath);
1013
- }
1014
- if (stagedSkills.length <= 0) {
1015
- throw new Error('agent_task_codex_skills_missing');
1016
- }
1017
- return stagedSkills.sort();
1018
- }
1019
932
  function assertAssignmentPluginBundleExtractionBounds(buffer) {
1020
933
  if (buffer.length < ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES) {
1021
934
  throw new Error('agent_task_plugin_bundle_invalid_zip');
@@ -1081,15 +994,102 @@ function assertAssignmentPluginBundleExtractionBounds(buffer) {
1081
994
  throw new Error('agent_task_plugin_bundle_invalid_zip');
1082
995
  }
1083
996
  }
997
+ function normalizeAgentBundlePath(root, value, code) {
998
+ const relativePath = typeof value === 'string' ? value.trim().replace(/\\/g, '/') : '';
999
+ if (!relativePath
1000
+ || relativePath.includes('\0')
1001
+ || node_path_1.default.posix.isAbsolute(relativePath)
1002
+ || relativePath === '..'
1003
+ || relativePath.startsWith('../')) {
1004
+ throw new Error(code);
1005
+ }
1006
+ const resolved = node_path_1.default.resolve(root, ...relativePath.split('/'));
1007
+ if (!isPathInside(root, resolved)) {
1008
+ throw new Error(code);
1009
+ }
1010
+ return resolved;
1011
+ }
1012
+ function readAgentBundleManifest(extractedRoot) {
1013
+ const manifestPath = node_path_1.default.join(extractedRoot, AGENT_BUNDLE_MANIFEST_PATH);
1014
+ if (!isFile(manifestPath)) {
1015
+ throw new Error('agent_bundle_manifest_missing');
1016
+ }
1017
+ let parsed;
1018
+ try {
1019
+ parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
1020
+ }
1021
+ catch {
1022
+ throw new Error('agent_bundle_manifest_invalid_json');
1023
+ }
1024
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1025
+ throw new Error('agent_bundle_manifest_invalid');
1026
+ }
1027
+ const manifest = parsed;
1028
+ if (manifest.version !== 1
1029
+ || !manifest.plugin
1030
+ || typeof manifest.plugin.root !== 'string'
1031
+ || !manifest.workspace
1032
+ || !Array.isArray(manifest.workspace.files)) {
1033
+ throw new Error('agent_bundle_manifest_invalid');
1034
+ }
1035
+ const pluginRoot = normalizeAgentBundlePath(extractedRoot, manifest.plugin.root, 'agent_bundle_plugin_root_invalid');
1036
+ if (!isFile(node_path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json'))) {
1037
+ throw new Error('agent_bundle_codex_plugin_manifest_missing');
1038
+ }
1039
+ if (!isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))) {
1040
+ throw new Error('agent_bundle_claude_plugin_manifest_missing');
1041
+ }
1042
+ const destinations = new Set();
1043
+ for (const entry of manifest.workspace.files) {
1044
+ const source = normalizeAgentBundlePath(extractedRoot, entry?.source, 'agent_bundle_workspace_source_invalid');
1045
+ if (!isFile(source)) {
1046
+ throw new Error(`agent_bundle_workspace_source_missing:${entry?.source ?? ''}`);
1047
+ }
1048
+ const destination = typeof entry?.destination === 'string'
1049
+ ? entry.destination.trim().replace(/\\/g, '/')
1050
+ : '';
1051
+ if (!destination
1052
+ || node_path_1.default.posix.isAbsolute(destination)
1053
+ || destination === '..'
1054
+ || destination.startsWith('../')) {
1055
+ throw new Error('agent_bundle_workspace_destination_invalid');
1056
+ }
1057
+ destinations.add(destination);
1058
+ }
1059
+ if (!destinations.has('AGENTS.md')) {
1060
+ throw new Error('agent_bundle_agents_file_missing');
1061
+ }
1062
+ if (!destinations.has('CLAUDE.md')) {
1063
+ throw new Error('agent_bundle_claude_file_missing');
1064
+ }
1065
+ return { manifest, pluginRoot };
1066
+ }
1067
+ async function stageAgentBundleWorkspaceFiles(extractedRoot, workspaceDir) {
1068
+ const resolved = readAgentBundleManifest(extractedRoot);
1069
+ for (const entry of resolved.manifest.workspace.files) {
1070
+ const source = normalizeAgentBundlePath(extractedRoot, entry.source, 'agent_bundle_workspace_source_invalid');
1071
+ const destination = resolveWorkspaceFileDestination(workspaceDir, entry.destination);
1072
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1073
+ await (0, promises_1.copyFile)(source, destination);
1074
+ }
1075
+ return resolved.pluginRoot;
1076
+ }
1084
1077
  async function stageAssignmentPluginBundle(input) {
1085
1078
  const sha256 = typeof input.pluginBundle.sha256 === 'string' ? input.pluginBundle.sha256.trim() : '';
1079
+ const fileName = typeof input.pluginBundle.fileName === 'string' ? input.pluginBundle.fileName.trim() : '';
1086
1080
  const contentBase64 = typeof input.pluginBundle.contentBase64 === 'string' ? input.pluginBundle.contentBase64.trim() : '';
1087
- if (!/^[a-f0-9]{64}$/.test(sha256) || !contentBase64) {
1081
+ const sizeBytes = Number(input.pluginBundle.sizeBytes);
1082
+ if (!/^[a-f0-9]{64}$/.test(sha256)
1083
+ || !fileName
1084
+ || node_path_1.default.basename(fileName) !== fileName
1085
+ || !Number.isInteger(sizeBytes)
1086
+ || sizeBytes <= 0
1087
+ || !contentBase64) {
1088
1088
  throw new Error('agent_task_plugin_bundle_invalid');
1089
1089
  }
1090
1090
  const buffer = Buffer.from(contentBase64, 'base64');
1091
- if (buffer.length <= 0) {
1092
- throw new Error('agent_task_plugin_bundle_empty');
1091
+ if (buffer.length !== sizeBytes) {
1092
+ throw new Error('agent_task_plugin_bundle_size_mismatch');
1093
1093
  }
1094
1094
  const actualSha = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
1095
1095
  if (actualSha !== sha256) {
@@ -1127,8 +1127,7 @@ async function stageAssignmentPluginBundle(input) {
1127
1127
  await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1128
1128
  await (0, promises_1.writeFile)(destination, Buffer.from(content));
1129
1129
  }
1130
- readPlaydropPluginStagingManifest(pluginRoot);
1131
- return pluginRoot;
1130
+ return await stageAgentBundleWorkspaceFiles(pluginRoot, input.workspaceDir);
1132
1131
  }
1133
1132
  function resolveDevPluginWorkingTree(input) {
1134
1133
  const workingTree = input.workingTree?.trim();
@@ -1144,7 +1143,7 @@ function resolveDevPluginWorkingTree(input) {
1144
1143
  throw new Error(`worker_dev_plugin_working_tree_requires_dev_environment:${input.env}`);
1145
1144
  }
1146
1145
  const resolved = node_path_1.default.resolve(workingTree);
1147
- readPlaydropPluginStagingManifest(resolved);
1146
+ readAgentBundleManifest(resolved);
1148
1147
  return resolved;
1149
1148
  }
1150
1149
  // A PlayDrop project root is the topmost directory holding a catalogue.json,
@@ -1502,6 +1501,7 @@ function buildWorkerCapabilities(input) {
1502
1501
  cliVersion: codexVersion,
1503
1502
  authenticated: codexAuthenticated,
1504
1503
  models: codexModels,
1504
+ acceptsCustomModels: true,
1505
1505
  ready: codexAuthenticated && codexModels.length > 0,
1506
1506
  });
1507
1507
  }
@@ -1517,6 +1517,7 @@ function buildWorkerCapabilities(input) {
1517
1517
  cliVersion: claudeVersion,
1518
1518
  authenticated: claudeAuthenticated,
1519
1519
  models: claudeModels,
1520
+ acceptsCustomModels: true,
1520
1521
  ready: claudeAuthenticated && claudeModels.length > 0,
1521
1522
  });
1522
1523
  }
@@ -1555,6 +1556,7 @@ function buildWorkerCapabilities(input) {
1555
1556
  }
1556
1557
  function buildWorkerClaimBody(input) {
1557
1558
  return {
1559
+ protocolVersion: 2,
1558
1560
  workerKey: input.workerKey,
1559
1561
  capabilities: {
1560
1562
  ...input.capabilities,
@@ -1562,89 +1564,43 @@ function buildWorkerClaimBody(input) {
1562
1564
  },
1563
1565
  };
1564
1566
  }
1565
- function resolveWorkerBaseSource(value) {
1566
- if (!value) {
1567
- throw new Error('agent_task_assignment_missing_base_source');
1568
- }
1569
- if (typeof value.appName !== 'string' || !value.appName.trim()) {
1570
- throw new Error('agent_task_base_app_name_missing');
1571
- }
1572
- if (typeof value.version !== 'string' || !value.version.trim()) {
1573
- throw new Error('agent_task_base_app_version_missing');
1574
- }
1575
- if (typeof value.appId !== 'number' || !Number.isInteger(value.appId) || value.appId <= 0) {
1576
- throw new Error('agent_task_base_app_id_missing');
1577
- }
1578
- if (typeof value.appVersionId !== 'number' || !Number.isInteger(value.appVersionId) || value.appVersionId <= 0) {
1579
- throw new Error('agent_task_base_app_version_id_missing');
1580
- }
1581
- return {
1582
- appName: value.appName.trim(),
1583
- version: value.version.trim(),
1584
- appId: value.appId,
1585
- appVersionId: value.appVersionId,
1586
- };
1587
- }
1588
- // Extracts a zip archive into targetDir using fflate (already a CLI
1589
- // dependency; see src/commands/create.ts). Entries that resolve outside the
1590
- // target directory throw instead of being skipped: a base source archive with
1591
- // traversal entries is corrupt and must fail the task loudly.
1567
+ // Extracts a server-provided workspace ZIP using fflate. Entries that resolve
1568
+ // outside the target directory fail the task immediately.
1592
1569
  function extractZipArchive(zipBuffer, targetDir) {
1593
1570
  const files = (0, fflate_1.unzipSync)(zipBuffer);
1594
1571
  const names = Object.keys(files);
1595
1572
  if (names.length === 0) {
1596
- throw new Error('agent_task_base_source_empty: the downloaded source archive contained no files.');
1573
+ throw new Error('agent_task_workspace_archive_empty');
1574
+ }
1575
+ if (names.length > ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES) {
1576
+ throw new Error(`agent_task_workspace_archive_too_many_entries:${names.length}>${ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES}`);
1597
1577
  }
1598
1578
  const resolvedTarget = node_path_1.default.resolve(targetDir);
1599
1579
  const written = [];
1580
+ let uncompressedBytes = 0;
1600
1581
  for (const originalName of names) {
1601
1582
  const name = originalName.replace(/\\/g, '/');
1583
+ if (!name || name.startsWith('/') || name.split('/').includes('..')) {
1584
+ throw new Error(`agent_task_workspace_archive_unsafe_entry:${originalName}`);
1585
+ }
1602
1586
  const destination = node_path_1.default.resolve(resolvedTarget, name);
1603
1587
  if (destination !== resolvedTarget && !destination.startsWith(resolvedTarget + node_path_1.default.sep)) {
1604
- throw new Error(`agent_task_base_source_unsafe_entry: archive entry "${originalName}" escapes the staging directory.`);
1588
+ throw new Error(`agent_task_workspace_archive_unsafe_entry:${originalName}`);
1605
1589
  }
1606
1590
  if (name.endsWith('/')) {
1607
1591
  (0, node_fs_1.mkdirSync)(destination, { recursive: true });
1608
1592
  continue;
1609
1593
  }
1594
+ uncompressedBytes += files[originalName].byteLength;
1595
+ if (uncompressedBytes > ASSIGNMENT_WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES) {
1596
+ throw new Error(`agent_task_workspace_archive_too_large:${uncompressedBytes}>${ASSIGNMENT_WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES}`);
1597
+ }
1610
1598
  (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(destination), { recursive: true });
1611
1599
  (0, node_fs_1.writeFileSync)(destination, Buffer.from(files[originalName]));
1612
1600
  written.push(name);
1613
1601
  }
1614
1602
  return written;
1615
1603
  }
1616
- // Reads the version string of the named app from the catalogue.json at the
1617
- // project root. Used for the staged base version and the version-bump guard.
1618
- function readWorkerProjectVersion(projectDir, appName) {
1619
- const cataloguePath = node_path_1.default.join(projectDir, 'catalogue.json');
1620
- let raw;
1621
- try {
1622
- raw = (0, node_fs_1.readFileSync)(cataloguePath, 'utf8');
1623
- }
1624
- catch (error) {
1625
- throw new Error(`agent_task_catalogue_unreadable: could not read ${cataloguePath} (${error instanceof Error ? error.message : String(error)}).`);
1626
- }
1627
- let parsed;
1628
- try {
1629
- parsed = JSON.parse(raw);
1630
- }
1631
- catch {
1632
- throw new Error(`agent_task_catalogue_invalid_json: ${cataloguePath} is not valid JSON.`);
1633
- }
1634
- const apps = parsed?.apps;
1635
- if (!Array.isArray(apps)) {
1636
- throw new Error(`agent_task_catalogue_apps_missing: ${cataloguePath} has no "apps" array.`);
1637
- }
1638
- const entry = apps.find((candidate) => Boolean(candidate) && typeof candidate === 'object' && candidate.name === appName);
1639
- if (!entry) {
1640
- throw new Error(`agent_task_catalogue_app_missing: no app named "${appName}" in ${cataloguePath}.`);
1641
- }
1642
- const version = typeof entry.version === 'string' ? entry.version.trim() : '';
1643
- if (!version) {
1644
- throw new Error(`agent_task_catalogue_version_missing: app "${appName}" in ${cataloguePath} has no version string.`);
1645
- }
1646
- return version;
1647
- }
1648
1604
  function normalizePreviewString(value) {
1649
1605
  const normalized = typeof value === 'string' ? value.trim() : '';
1650
1606
  return normalized || undefined;
@@ -1827,52 +1783,40 @@ async function drainWorkerEventQueue(input) {
1827
1783
  (0, node_fs_1.rmSync)(filePath, { force: true });
1828
1784
  }
1829
1785
  }
1830
- // Pre-publish guard for GAME_UPDATE tasks: the server rejects a duplicate
1831
- // version anyway, but failing here gives the task a precise error code.
1832
- function assertWorkerProjectVersionBumped(input) {
1833
- if (input.projectVersion === input.baseVersion) {
1834
- throw new Error(`agent_task_version_not_bumped: app "${input.appName}" version is still "${input.baseVersion}"; the update must bump the catalogue.json version before publish.`);
1835
- }
1836
- }
1837
- async function removeStaleWorkerControlFilesFromBaseSource(stagingDir) {
1786
+ async function removeStaleWorkerControlFiles(stagingDir) {
1838
1787
  await (0, promises_1.rm)(node_path_1.default.join(stagingDir, '.playdrop.json'), { force: true });
1839
1788
  }
1840
- // Downloads the base app source archive and unzips it into the workspace so
1841
- // the project root (catalogue.json) lands inside the workspace before Codex
1842
- // starts. Returns the staged project root and its catalogue version string.
1843
- async function stageWorkerBaseAppSource(input) {
1844
- const { blob } = await input.client.downloadAppSource(input.creatorUsername, input.baseApp.appName, input.baseApp.version);
1845
- const buffer = Buffer.from(await blob.arrayBuffer());
1846
- const stagingDir = node_path_1.default.join(input.workspaceDir, input.baseApp.appName);
1847
- (0, node_fs_1.mkdirSync)(stagingDir, { recursive: true });
1848
- extractZipArchive(new Uint8Array(buffer), stagingDir);
1849
- await removeStaleWorkerControlFilesFromBaseSource(stagingDir);
1850
- const stagedProjectDir = discoverWorkerProjectRoot(stagingDir);
1851
- const stagedBaseVersion = readWorkerProjectVersion(stagedProjectDir, input.baseApp.appName);
1852
- return { stagedProjectDir, stagedBaseVersion };
1853
- }
1854
- function resolveCodexModel(model) {
1789
+ function resolveCodexModel(model, reasoningEffortOverride) {
1855
1790
  const normalized = model.trim();
1856
1791
  if (!normalized) {
1857
1792
  throw new Error('agent_task_assignment_model_missing');
1858
1793
  }
1859
- const effortMatch = normalized.match(/^(.*?)-(extra-high|low|medium|high|xhigh|max)$/);
1794
+ const effortMatch = normalized.match(/^(.*?)-(extra-high|low|medium|high|xhigh|max|ultra)$/);
1795
+ const legacyEffort = effortMatch?.[2] === 'extra-high' ? 'xhigh' : effortMatch?.[2];
1796
+ const reasoningEffort = reasoningEffortOverride?.trim() || legacyEffort || 'medium';
1797
+ if (reasoningEffort.length > 40 || /\s/.test(reasoningEffort)) {
1798
+ throw new Error('invalid_agent_task_reasoning_effort');
1799
+ }
1860
1800
  if (effortMatch?.[1] && effortMatch[2]) {
1861
- const effort = effortMatch[2] === 'extra-high' ? 'xhigh' : effortMatch[2];
1862
- return { model: effortMatch[1], reasoningEffort: effort };
1801
+ return { model: effortMatch[1], reasoningEffort };
1863
1802
  }
1864
- return { model: normalized, reasoningEffort: 'medium' };
1803
+ return { model: normalized, reasoningEffort };
1865
1804
  }
1866
- function resolveClaudeModel(model) {
1805
+ function resolveClaudeModel(model, reasoningEffortOverride) {
1867
1806
  const normalized = model.trim();
1868
1807
  if (!normalized) {
1869
1808
  throw new Error('agent_task_assignment_model_missing');
1870
1809
  }
1871
- const effortMatch = normalized.match(/^(.*)-(low|medium|high|xhigh|max)$/);
1810
+ const effortMatch = normalized.match(/^(.*)-(extra-high|low|medium|high|xhigh|max|ultra)$/);
1811
+ const legacyEffort = effortMatch?.[2] === 'extra-high' ? 'xhigh' : effortMatch?.[2] ?? null;
1812
+ const effort = reasoningEffortOverride?.trim() || legacyEffort;
1813
+ if (effort && (effort.length > 40 || /\s/.test(effort))) {
1814
+ throw new Error('invalid_agent_task_reasoning_effort');
1815
+ }
1872
1816
  if (effortMatch?.[1] && effortMatch[2]) {
1873
- return { model: resolveClaudeRuntimeModel(effortMatch[1]), effort: effortMatch[2] };
1817
+ return { model: resolveClaudeRuntimeModel(effortMatch[1]), effort };
1874
1818
  }
1875
- return { model: resolveClaudeRuntimeModel(normalized), effort: null };
1819
+ return { model: resolveClaudeRuntimeModel(normalized), effort };
1876
1820
  }
1877
1821
  function resolveClaudeRuntimeModel(model) {
1878
1822
  if (model === 'claude-haiku-4-5') {
@@ -1987,6 +1931,21 @@ async function runCodex(input) {
1987
1931
  workerUsername: input.workerUsername,
1988
1932
  envName: input.envName,
1989
1933
  });
1934
+ if (input.taskPluginRoot) {
1935
+ const pluginEnv = {
1936
+ ...node_process_1.default.env,
1937
+ HOME: homeDir,
1938
+ CODEX_HOME: codexHomeDir,
1939
+ };
1940
+ await execFileAsync('codex', ['plugin', 'marketplace', 'add', input.taskPluginRoot, '--json'], {
1941
+ cwd: input.workspaceDir,
1942
+ env: pluginEnv,
1943
+ });
1944
+ await execFileAsync('codex', ['plugin', 'add', 'playdrop@playdrop', '--json'], {
1945
+ cwd: input.workspaceDir,
1946
+ env: pluginEnv,
1947
+ });
1948
+ }
1990
1949
  let observedCodexThreadId = null;
1991
1950
  let codexThreadProbe = '';
1992
1951
  const result = await (0, runtime_1.runLoggedProcess)({
@@ -1997,11 +1956,13 @@ async function runCodex(input) {
1997
1956
  reasoningEffort: input.codexModel.reasoningEffort,
1998
1957
  networkAccess: (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_AGENT_NETWORK_ACCESS', false),
1999
1958
  sandboxMode: (0, runtime_1.readCodexSandboxMode)(),
1959
+ imagePaths: input.imagePaths,
2000
1960
  }),
2001
1961
  cwd: input.workspaceDir,
2002
1962
  env: (0, runtime_1.buildWorkerChildEnv)({
2003
1963
  binDir: input.binDir,
2004
1964
  taskId: input.taskId,
1965
+ taskToken: input.taskToken,
2005
1966
  attempt: input.attempt,
2006
1967
  envName: input.envName,
2007
1968
  eventDir: input.eventDir,
@@ -2082,6 +2043,7 @@ async function runClaude(input) {
2082
2043
  env: (0, runtime_1.buildWorkerChildEnv)({
2083
2044
  binDir: input.binDir,
2084
2045
  taskId: input.taskId,
2046
+ taskToken: input.taskToken,
2085
2047
  attempt: input.attempt,
2086
2048
  envName: input.envName,
2087
2049
  eventDir: input.eventDir,
@@ -2089,7 +2051,23 @@ async function runClaude(input) {
2089
2051
  playdropConfigPath,
2090
2052
  playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
2091
2053
  }),
2092
- stdin: input.prompt,
2054
+ stdin: `${JSON.stringify({
2055
+ type: 'user',
2056
+ message: {
2057
+ role: 'user',
2058
+ content: [
2059
+ { type: 'text', text: input.prompt },
2060
+ ...input.imageAttachments.map((attachment) => ({
2061
+ type: 'image',
2062
+ source: {
2063
+ type: 'base64',
2064
+ media_type: attachment.contentType,
2065
+ data: (0, node_fs_1.readFileSync)(attachment.path).toString('base64'),
2066
+ },
2067
+ })),
2068
+ ],
2069
+ },
2070
+ })}\n`,
2093
2071
  timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2094
2072
  maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
2095
2073
  onTranscriptChunks: input.onTranscriptChunks,
@@ -2139,6 +2117,13 @@ function buildAgentRunResult(result) {
2139
2117
  tokensUsed: result.tokensUsed,
2140
2118
  };
2141
2119
  }
2120
+ function agentFailureRawError(result) {
2121
+ return [result.stderr, result.stdout, result.outputTail]
2122
+ .map((value) => value.trim())
2123
+ .filter(Boolean)
2124
+ .join('\n')
2125
+ || 'agent_failure';
2126
+ }
2142
2127
  function buildSupervisorFailureRunResult(message) {
2143
2128
  return {
2144
2129
  exitCode: null,
@@ -2159,22 +2144,33 @@ function buildSupervisorFailureRunResult(message) {
2159
2144
  tokensUsed: null,
2160
2145
  };
2161
2146
  }
2162
- function stripStagedPlaydropPluginPrefix(value) {
2163
- const normalized = value.trim().replace(/\\/g, '/');
2164
- const pluginPrefix = `${STAGED_PLAYDROP_PLUGIN_ROOT}/`;
2165
- if (normalized.startsWith(pluginPrefix)) {
2166
- return normalized.slice(pluginPrefix.length);
2147
+ function listPluginSkillPaths(pluginRoot) {
2148
+ if (!pluginRoot) {
2149
+ return [];
2167
2150
  }
2168
- return normalized;
2169
- }
2170
- function normalizeTelemetrySkillPaths(stagedPaths) {
2171
- return Array.from(new Set(stagedPaths.map(stripStagedPlaydropPluginPrefix).filter(Boolean))).sort();
2151
+ const skillsRoot = node_path_1.default.join(pluginRoot, 'skills');
2152
+ if (!(0, node_fs_1.existsSync)(skillsRoot)) {
2153
+ return [];
2154
+ }
2155
+ const paths = [];
2156
+ const visit = (directory) => {
2157
+ for (const entry of (0, node_fs_1.readdirSync)(directory, { withFileTypes: true })) {
2158
+ const entryPath = node_path_1.default.join(directory, entry.name);
2159
+ if (entry.isDirectory()) {
2160
+ visit(entryPath);
2161
+ }
2162
+ else if (entry.isFile() && entry.name === 'SKILL.md') {
2163
+ paths.push(node_path_1.default.relative(pluginRoot, entryPath).replace(/\\/g, '/'));
2164
+ }
2165
+ }
2166
+ };
2167
+ visit(skillsRoot);
2168
+ return paths.sort();
2172
2169
  }
2173
2170
  function collectObservedPlaydropSkillPaths(output, availablePaths) {
2174
2171
  const observed = new Set();
2175
2172
  for (const availablePath of availablePaths) {
2176
- if (output.includes(availablePath)
2177
- || output.includes(`${STAGED_PLAYDROP_PLUGIN_ROOT}/${availablePath}`)) {
2173
+ if (output.includes(availablePath)) {
2178
2174
  observed.add(availablePath);
2179
2175
  }
2180
2176
  }
@@ -2195,8 +2191,8 @@ async function recordAgentRunTelemetry(input) {
2195
2191
  workerKey: input.workerKey,
2196
2192
  leaseToken: input.leaseToken,
2197
2193
  attempt: input.task.attempts,
2198
- agent: input.assignment.agent,
2199
- requestedModel: input.assignment.model,
2194
+ agent: input.assignment.agent.runtime,
2195
+ requestedModel: input.assignment.agent.model,
2200
2196
  resolvedRuntimeModel: input.resolvedRuntimeModel,
2201
2197
  reasoningEffort: input.reasoningEffort,
2202
2198
  status: resolveTelemetryStatus(input.status),
@@ -2216,6 +2212,11 @@ async function recordAgentRunTelemetry(input) {
2216
2212
  observedSkillPaths: [...observedSkillPaths].sort(),
2217
2213
  startedAt: input.startedAt.toISOString(),
2218
2214
  completedAt: input.completedAt.toISOString(),
2215
+ ...(input.status === 'FAILED'
2216
+ ? {
2217
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(agentFailureRawError(input.result), 'agent'),
2218
+ }
2219
+ : {}),
2219
2220
  });
2220
2221
  }
2221
2222
  function providerNameForAgent(agent) {
@@ -2232,9 +2233,14 @@ function extractProviderStatusCode(text) {
2232
2233
  }
2233
2234
  return null;
2234
2235
  }
2235
- function buildAgentFailureCode(agent, result) {
2236
+ function buildAgentFailureCode(agent, result, request) {
2236
2237
  const provider = providerNameForAgent(agent);
2237
2238
  const combinedOutput = `${result.outputTail}\n${result.stderr}\n${result.stdout}`;
2239
+ if (request
2240
+ && (/\b(?:unknown|invalid|unsupported)\s+(?:model|effort)\b/i.test(combinedOutput)
2241
+ || /\b(?:model|effort)\b.{0,80}\b(?:not found|not available|does not exist|is unavailable|is not supported)\b/i.test(combinedOutput))) {
2242
+ return `requested_model_unavailable:${agent}:${request.model}:${request.reasoningEffort ?? ''}`;
2243
+ }
2238
2244
  const providerStatus = extractProviderStatusCode(combinedOutput);
2239
2245
  if (providerStatus === 529 || (/\b529\b/.test(combinedOutput) && /\boverloaded?\b/i.test(combinedOutput))) {
2240
2246
  return `agent_provider_overloaded:${provider}:529`;
@@ -2255,6 +2261,9 @@ function buildAgentFailureCode(agent, result) {
2255
2261
  }
2256
2262
  function describeAgentFailureForEvent(errorCode) {
2257
2263
  const [code, provider, detail] = errorCode.split(':');
2264
+ if (code === 'requested_model_unavailable') {
2265
+ return 'The requested model or reasoning effort is not available on this worker. Choose another exact configuration and retry.';
2266
+ }
2258
2267
  const providerLabel = provider === 'claude' ? 'Claude' : provider === 'codex' ? 'Codex' : 'The agent provider';
2259
2268
  if (code === 'agent_provider_overloaded') {
2260
2269
  return detail
@@ -2681,14 +2690,6 @@ function buildWorkerSetupActions(input) {
2681
2690
  message: 'The PlayDrop CLI package could not load playwright-core from its installed dependencies.',
2682
2691
  });
2683
2692
  }
2684
- if (reasons.has('playdrop_plugin_staging_manifest_missing')) {
2685
- actions.push({
2686
- reason: 'playdrop_plugin_staging_manifest_missing',
2687
- label: 'Install or update the PlayDrop agent plugin',
2688
- command: 'playdrop agents status --json',
2689
- message: 'Run the CLI-reported nextAction.command for a supported local agent so playdrop-worker-staging.json is available.',
2690
- });
2691
- }
2692
2693
  if (reasons.has('agent_cli_not_found')) {
2693
2694
  actions.push({
2694
2695
  reason: 'agent_cli_not_found',
@@ -3559,8 +3560,14 @@ async function setupWorker(options = {}) {
3559
3560
  }, { env: options.env });
3560
3561
  }
3561
3562
  async function failTaskWithRetry(client, taskId, body) {
3563
+ const terminalReason = body.terminalReason ?? (0, types_1.classifyAgentTaskTerminalReason)(body.error, 'worker');
3564
+ const requestBody = {
3565
+ ...body,
3566
+ error: terminalReason.code,
3567
+ terminalReason,
3568
+ };
3562
3569
  try {
3563
- await client.workerFailAgentTask(taskId, body);
3570
+ await client.workerFailAgentTask(taskId, requestBody);
3564
3571
  }
3565
3572
  catch (firstError) {
3566
3573
  if (isAgentTaskNotRunningError(firstError)) {
@@ -3568,7 +3575,7 @@ async function failTaskWithRetry(client, taskId, body) {
3568
3575
  }
3569
3576
  await sleep(FAIL_REPORT_RETRY_DELAY_MS);
3570
3577
  try {
3571
- await client.workerFailAgentTask(taskId, body);
3578
+ await client.workerFailAgentTask(taskId, requestBody);
3572
3579
  }
3573
3580
  catch (secondError) {
3574
3581
  if (isAgentTaskNotRunningError(secondError)) {
@@ -3588,13 +3595,24 @@ async function fetchTaskDetail(client, target, taskId) {
3588
3595
  return await client.getAgentTask(taskId);
3589
3596
  }
3590
3597
  function instrumentTaskChromeTitleMatchers(assignment) {
3591
- const targets = assignment.kind === 'GAME_REVIEW'
3592
- ? [assignment.inputs.reviewTarget]
3593
- : assignment.kind === 'GAME_EVAL'
3594
- ? assignment.inputs.evalTarget?.targets ?? []
3598
+ const playdrop = assignment.task.metadata.playdrop;
3599
+ const metadata = playdrop && typeof playdrop === 'object' && !Array.isArray(playdrop)
3600
+ ? playdrop
3601
+ : {};
3602
+ const kind = metadata.kind;
3603
+ const evalTarget = metadata.evalTarget && typeof metadata.evalTarget === 'object' && !Array.isArray(metadata.evalTarget)
3604
+ ? metadata.evalTarget
3605
+ : null;
3606
+ const targets = kind === 'GAME_REVIEW'
3607
+ ? [metadata.reviewTarget]
3608
+ : kind === 'GAME_EVAL' && Array.isArray(evalTarget?.targets)
3609
+ ? evalTarget.targets
3595
3610
  : [];
3596
3611
  const matchers = new Set();
3597
- for (const target of targets) {
3612
+ for (const rawTarget of targets) {
3613
+ const target = rawTarget && typeof rawTarget === 'object' && !Array.isArray(rawTarget)
3614
+ ? rawTarget
3615
+ : null;
3598
3616
  if (!target)
3599
3617
  continue;
3600
3618
  for (const value of [target.appDisplayName, target.appName]) {
@@ -3687,7 +3705,7 @@ on run matchers
3687
3705
  end run
3688
3706
  `;
3689
3707
  async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3690
- if (assignment.kind !== 'GAME_REVIEW' && assignment.kind !== 'GAME_EVAL') {
3708
+ if (assignment.agent.browser === 'NONE') {
3691
3709
  return { remainingTaskTabs: 0, blankWindows: 0 };
3692
3710
  }
3693
3711
  if ((options.platform ?? node_process_1.default.platform) !== 'darwin') {
@@ -3895,11 +3913,19 @@ async function startWorker(options = {}) {
3895
3913
  });
3896
3914
  }, HEARTBEAT_INTERVAL_MS);
3897
3915
  const runClaimedTask = async (claim, devPort) => {
3898
- const task = claim.task;
3899
- if (!task) {
3900
- throw new Error('agent_task_claim_missing_task');
3916
+ const claimedAssignment = normalizeWorkerTaskAssignmentV2(claim);
3917
+ if (!claimedAssignment) {
3918
+ throw new Error('agent_task_claim_missing_assignment');
3901
3919
  }
3902
- const leaseToken = claim.leaseToken?.trim();
3920
+ const taskContext = buildWorkerTaskContextV2(claimedAssignment);
3921
+ const task = {
3922
+ id: claimedAssignment.task.id,
3923
+ kind: taskContext.kind,
3924
+ model: claimedAssignment.agent.model,
3925
+ executionTarget: claimedAssignment.task.executionTarget,
3926
+ attempts: claimedAssignment.task.attempt,
3927
+ };
3928
+ const leaseToken = claim.action === 'run' ? claim.lease.token.trim() : '';
3903
3929
  if (!leaseToken) {
3904
3930
  throw new Error('agent_task_claim_missing_lease_token');
3905
3931
  }
@@ -3914,7 +3940,7 @@ async function startWorker(options = {}) {
3914
3940
  let workspaceDir = null;
3915
3941
  let availableSkillPaths = [];
3916
3942
  const observedSkillPaths = new Set();
3917
- let assignment = null;
3943
+ const assignment = claimedAssignment;
3918
3944
  let resolvedRuntimeModel = task.model;
3919
3945
  let reasoningEffort = null;
3920
3946
  let agentStartedAt = null;
@@ -3923,9 +3949,6 @@ async function startWorker(options = {}) {
3923
3949
  let telemetryReported = false;
3924
3950
  let heartbeatTransientFailures = 0;
3925
3951
  const reportTelemetry = async (status, setupError) => {
3926
- if (!assignment) {
3927
- return;
3928
- }
3929
3952
  const completedAt = agentCompletedAt ?? new Date();
3930
3953
  await recordAgentRunTelemetry({
3931
3954
  client,
@@ -3945,20 +3968,19 @@ async function startWorker(options = {}) {
3945
3968
  telemetryReported = true;
3946
3969
  };
3947
3970
  try {
3948
- assignment = resolveWorkerClaimTaskAssignment(claim);
3949
- if (!assignment) {
3950
- throw new Error('agent_task_claim_missing_task_assignment');
3951
- }
3952
- const codexModel = assignment.agent === 'CODEX' ? resolveCodexModel(assignment.model) : null;
3953
- const claudeModel = assignment.agent === 'CLAUDE_CODE' ? resolveClaudeModel(assignment.model) : null;
3954
- resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.model;
3971
+ const codexModel = assignment.agent.runtime === 'CODEX'
3972
+ ? resolveCodexModel(assignment.agent.model, assignment.agent.reasoningEffort)
3973
+ : null;
3974
+ const claudeModel = assignment.agent.runtime === 'CLAUDE_CODE'
3975
+ ? resolveClaudeModel(assignment.agent.model, assignment.agent.reasoningEffort)
3976
+ : null;
3977
+ resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.agent.model;
3955
3978
  reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
3956
- const taskContext = buildWorkerTaskContext(claim, assignment);
3957
3979
  workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
3958
3980
  await pruneWorkerTaskWorkspaces(workspaceDir);
3959
3981
  await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
3960
3982
  await (0, promises_1.mkdir)(workspaceDir, { recursive: true });
3961
- const eventDir = node_path_1.default.join(workspaceDir, '.playdrop-task-events');
3983
+ const eventDir = node_path_1.default.join(workspaceDir, '.playdrop', 'task-events');
3962
3984
  await (0, promises_1.mkdir)(eventDir, { recursive: true });
3963
3985
  await stageWorkerTaskContext({
3964
3986
  workspaceDir,
@@ -4003,52 +4025,16 @@ async function startWorker(options = {}) {
4003
4025
  appendObservedSkillPathsFromTranscript(chunks);
4004
4026
  await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
4005
4027
  };
4006
- await client.workerCreateAgentTaskEvent(task.id, {
4007
- workerKey,
4008
- leaseToken,
4009
- kind: 'progress',
4010
- phase: 'setup',
4011
- message: 'Preparing worker workspace',
4012
- pct: 2,
4013
- });
4014
- const assignmentPluginRoot = devPluginWorkingTree ?? await stageAssignmentPluginBundle({
4015
- workspaceDir,
4016
- pluginBundle: assignment.pluginBundle,
4017
- });
4018
- const stagedPluginPaths = await stagePlaydropPluginReferences({
4019
- workspaceDir,
4020
- pluginRoot: assignmentPluginRoot,
4021
- kind: task.kind,
4022
- });
4023
- if (assignment.agent === 'CODEX') {
4024
- await stageCodexTaskSkills({ workspaceDir, stagedPluginPaths });
4025
- }
4026
- availableSkillPaths = normalizeTelemetrySkillPaths(stagedPluginPaths);
4027
- await client.workerCreateAgentTaskEvent(task.id, {
4028
- workerKey,
4029
- leaseToken,
4030
- kind: 'progress',
4031
- phase: 'setup',
4032
- message: devPluginWorkingTree
4033
- ? 'Staged PlayDrop plugin references from explicit dev working tree'
4034
- : 'Staged PlayDrop plugin references from server task bundle',
4035
- pct: 3,
4036
- payload: {
4037
- pluginSource: devPluginWorkingTree ? 'DEV_WORKING_TREE' : 'SERVER_TASK_BUNDLE',
4038
- bundleSha: assignment.pluginBundle.sha256,
4039
- },
4040
- });
4041
- if (assignment.workspace.files.length > 0) {
4042
- await client.workerCreateAgentTaskEvent(task.id, {
4043
- workerKey,
4044
- leaseToken,
4045
- kind: 'progress',
4046
- phase: 'setup',
4047
- message: `Staging ${assignment.workspace.files.length} server-provided workspace file${assignment.workspace.files.length === 1 ? '' : 's'}`,
4048
- pct: 3,
4049
- });
4050
- await stageAssignmentWorkspaceFiles(workspaceDir, assignment.workspace.files);
4051
- }
4028
+ await stageAssignmentWorkspaceV2(workspaceDir, assignment.workspace);
4029
+ const assignmentPluginRoot = devPluginWorkingTree
4030
+ ? await stageAgentBundleWorkspaceFiles(devPluginWorkingTree, workspaceDir)
4031
+ : assignment.plugin
4032
+ ? await stageAssignmentPluginBundle({
4033
+ workspaceDir,
4034
+ pluginBundle: assignment.plugin,
4035
+ })
4036
+ : null;
4037
+ availableSkillPaths = listPluginSkillPaths(assignmentPluginRoot);
4052
4038
  heartbeatTimer = setInterval(() => {
4053
4039
  client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
4054
4040
  .then((heartbeat) => {
@@ -4107,57 +4093,18 @@ async function startWorker(options = {}) {
4107
4093
  handleEventDrainFailure(error);
4108
4094
  });
4109
4095
  }, 1000);
4110
- const prompt = assignment.prompt;
4111
- if (task.kind === 'GAME_UPDATE') {
4112
- const baseSourcePayload = assignment.inputs.baseSource;
4113
- const baseSource = resolveWorkerBaseSource(baseSourcePayload);
4114
- await client.workerCreateAgentTaskEvent(task.id, {
4115
- workerKey,
4116
- leaseToken,
4117
- kind: 'progress',
4118
- phase: 'setup',
4119
- message: `Staging base source ${baseSource.appName} version ${baseSource.version} into the workspace`,
4120
- pct: 4,
4121
- });
4122
- await stageWorkerBaseAppSource({
4123
- client,
4124
- workspaceDir,
4125
- creatorUsername: baseSourcePayload.creatorUsername,
4126
- baseApp: baseSource,
4127
- });
4128
- }
4129
- else if (task.kind === 'NEW_GAME' || task.kind === 'REMIX_GAME') {
4130
- if (assignment.inputs.baseSource !== null) {
4131
- throw new Error('agent_task_assignment_unexpected_base_source');
4132
- }
4133
- if (task.kind === 'REMIX_GAME' && !assignment.inputs.remixSource) {
4134
- throw new Error('agent_task_assignment_remix_source_missing');
4135
- }
4136
- }
4137
- else if (task.kind === 'GAME_REVIEW') {
4138
- if (assignment.inputs.baseSource !== null) {
4139
- throw new Error('agent_task_assignment_unexpected_base_source');
4140
- }
4141
- if (!assignment.inputs.reviewTarget) {
4142
- throw new Error('agent_task_assignment_review_target_missing');
4143
- }
4144
- }
4145
- else if (task.kind === 'GAME_EVAL') {
4146
- if (assignment.inputs.baseSource !== null) {
4147
- throw new Error('agent_task_assignment_unexpected_base_source');
4148
- }
4149
- if (!assignment.inputs.evalTarget) {
4150
- throw new Error('agent_task_assignment_eval_target_missing');
4151
- }
4152
- }
4153
- else {
4154
- throw new Error(`unsupported_agent_task_kind:${task.kind}`);
4155
- }
4096
+ const prompt = assignment.request.prompt;
4097
+ const imageAttachments = assignment.request.attachments
4098
+ .filter((attachment) => attachment.contentType.startsWith('image/'))
4099
+ .map((attachment) => ({
4100
+ path: resolveWorkspaceFileDestination(workspaceDir, attachment.path),
4101
+ contentType: attachment.contentType,
4102
+ }));
4156
4103
  if (shuttingDown) {
4157
4104
  throw new Error('worker_shutdown');
4158
4105
  }
4159
4106
  agentStartedAt = new Date();
4160
- if (assignment.agent === 'CODEX') {
4107
+ if (assignment.agent.runtime === 'CODEX') {
4161
4108
  agentResult = await runCodex({
4162
4109
  workspaceDir,
4163
4110
  binDir,
@@ -4166,9 +4113,12 @@ async function startWorker(options = {}) {
4166
4113
  envName: env,
4167
4114
  workerUsername: username,
4168
4115
  taskId: task.id,
4116
+ taskToken: assignment.task.token,
4169
4117
  attempt: taskContext.attempt,
4170
4118
  devPort,
4171
- codexModel: codexModel ?? resolveCodexModel(assignment.model),
4119
+ codexModel: codexModel ?? resolveCodexModel(assignment.agent.model),
4120
+ taskPluginRoot: assignmentPluginRoot,
4121
+ imagePaths: imageAttachments.map((attachment) => attachment.path),
4172
4122
  onTranscriptChunks: async (chunks) => {
4173
4123
  await appendTranscriptChunks(chunks);
4174
4124
  },
@@ -4177,21 +4127,23 @@ async function startWorker(options = {}) {
4177
4127
  },
4178
4128
  });
4179
4129
  }
4180
- else if (assignment.agent === 'CLAUDE_CODE') {
4130
+ else if (assignment.agent.runtime === 'CLAUDE_CODE') {
4181
4131
  agentResult = await runClaude({
4182
4132
  workspaceDir,
4183
4133
  binDir,
4184
4134
  eventDir,
4185
4135
  prompt,
4186
- enableChrome: false,
4187
- enablePlaywrightMcp: task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL',
4136
+ enableChrome: assignment.agent.browser === 'CHROME',
4137
+ enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4188
4138
  envName: env,
4189
4139
  workerUsername: username,
4190
4140
  taskId: task.id,
4141
+ taskToken: assignment.task.token,
4191
4142
  attempt: taskContext.attempt,
4192
4143
  devPort,
4193
- claudeModel: claudeModel ?? resolveClaudeModel(assignment.model),
4144
+ claudeModel: claudeModel ?? resolveClaudeModel(assignment.agent.model),
4194
4145
  taskPluginRoot: assignmentPluginRoot,
4146
+ imageAttachments,
4195
4147
  onTranscriptChunks: async (chunks) => {
4196
4148
  await appendTranscriptChunks(chunks);
4197
4149
  },
@@ -4200,17 +4152,11 @@ async function startWorker(options = {}) {
4200
4152
  },
4201
4153
  });
4202
4154
  }
4203
- else if (assignment.agent === 'CURSOR_COMPOSER') {
4204
- const assignmentAgent = assignment.agent;
4205
- const assignmentModel = assignment.model;
4206
- const plannedAttemptIndex = assignment.attemptPlan?.options.findIndex((option) => option.agent === assignmentAgent && option.model === assignmentModel);
4207
- const attemptIndex = typeof plannedAttemptIndex === 'number' && plannedAttemptIndex >= 0
4208
- ? plannedAttemptIndex
4209
- : 0;
4155
+ else if (assignment.agent.runtime === 'CURSOR_COMPOSER') {
4210
4156
  await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4211
4157
  workerKey,
4212
4158
  leaseToken,
4213
- attemptIndex,
4159
+ attemptIndex: 0,
4214
4160
  reason: 'unavailable_runtime',
4215
4161
  message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4216
4162
  });
@@ -4218,7 +4164,7 @@ async function startWorker(options = {}) {
4218
4164
  throw new Error('worker_cursor_composer_runner_not_configured');
4219
4165
  }
4220
4166
  else {
4221
- throw new Error(`unsupported_worker_agent:${assignment.agent}`);
4167
+ throw new Error(`unsupported_worker_agent:${assignment.agent.runtime}`);
4222
4168
  }
4223
4169
  agentCompletedAt = new Date();
4224
4170
  await queueEventDrain().catch((error) => {
@@ -4277,7 +4223,10 @@ async function startWorker(options = {}) {
4277
4223
  }
4278
4224
  else {
4279
4225
  retainWorkspace = true;
4280
- const failureCode = buildAgentFailureCode(assignment.agent, completedAgentResult);
4226
+ const failureCode = buildAgentFailureCode(assignment.agent.runtime, completedAgentResult, {
4227
+ model: assignment.agent.model,
4228
+ reasoningEffort,
4229
+ });
4281
4230
  await client.workerCreateAgentTaskEvent(task.id, {
4282
4231
  workerKey,
4283
4232
  leaseToken,
@@ -4291,6 +4240,7 @@ async function startWorker(options = {}) {
4291
4240
  workerKey,
4292
4241
  leaseToken,
4293
4242
  error: failureCode,
4243
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(agentFailureRawError(completedAgentResult), 'agent'),
4294
4244
  result: agentRunResult,
4295
4245
  });
4296
4246
  await reportTelemetry('FAILED');
@@ -4377,6 +4327,7 @@ async function startWorker(options = {}) {
4377
4327
  workerKey,
4378
4328
  leaseToken,
4379
4329
  error: normalizeWorkerFailureErrorCode(message),
4330
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'worker'),
4380
4331
  result: { workerError: message },
4381
4332
  });
4382
4333
  }
@@ -4399,9 +4350,8 @@ async function startWorker(options = {}) {
4399
4350
  if (eventDrainTimer) {
4400
4351
  clearInterval(eventDrainTimer);
4401
4352
  }
4402
- if (assignment
4403
- && agentStartedAt
4404
- && (assignment.kind === 'GAME_REVIEW' || assignment.kind === 'GAME_EVAL')) {
4353
+ if (agentStartedAt
4354
+ && assignment.agent.browser !== 'NONE') {
4405
4355
  try {
4406
4356
  const hygiene = await enforceInstrumentBrowserHygiene(assignment);
4407
4357
  console.log(`Agent task ${task.id} browser hygiene passed: ${hygiene.remainingTaskTabs} task tabs, ${hygiene.blankWindows} blank window(s).`);
@@ -4475,8 +4425,7 @@ async function startWorker(options = {}) {
4475
4425
  claimBackoffMs = nextClaimBackoffMs(claimBackoffMs);
4476
4426
  continue;
4477
4427
  }
4478
- const task = claim.task;
4479
- if (!task) {
4428
+ if (claim.action !== 'run') {
4480
4429
  if (shuttingDown || quiescingForUpdate) {
4481
4430
  break;
4482
4431
  }
@@ -4486,18 +4435,19 @@ async function startWorker(options = {}) {
4486
4435
  await sleep(DEFAULT_POLL_INTERVAL_MS);
4487
4436
  continue;
4488
4437
  }
4489
- activeTaskIds.add(task.id);
4438
+ const taskId = claim.assignment.task.id;
4439
+ activeTaskIds.add(taskId);
4490
4440
  activeDevPorts.add(devPort);
4491
4441
  const taskRun = runClaimedTask(claim, devPort)
4492
4442
  .catch((error) => {
4493
4443
  recordFatalTaskError(error);
4494
4444
  })
4495
4445
  .finally(() => {
4496
- activeTaskRuns.delete(task.id);
4497
- activeTaskIds.delete(task.id);
4446
+ activeTaskRuns.delete(taskId);
4447
+ activeTaskIds.delete(taskId);
4498
4448
  activeDevPorts.delete(devPort);
4499
4449
  });
4500
- activeTaskRuns.set(task.id, taskRun);
4450
+ activeTaskRuns.set(taskId, taskRun);
4501
4451
  if (options.once) {
4502
4452
  await taskRun;
4503
4453
  if (fatalTaskError) {
@@ -4594,6 +4544,21 @@ async function reportTask(options) {
4594
4544
  }
4595
4545
  resolveWorkerTaskStateForWrite();
4596
4546
  }
4547
+ function showTaskContext() {
4548
+ const taskContext = readTaskContextFile();
4549
+ console.log(JSON.stringify({
4550
+ task: {
4551
+ id: taskContext.taskId,
4552
+ type: taskContext.taskType,
4553
+ attempt: taskContext.attempt,
4554
+ executionTarget: taskContext.target,
4555
+ metadata: taskContext.metadata,
4556
+ },
4557
+ request: {
4558
+ prompt: taskContext.creatorRequest,
4559
+ },
4560
+ }, null, 2));
4561
+ }
4597
4562
  async function reportCatalogueTask(options) {
4598
4563
  const message = options.message?.trim();
4599
4564
  if (!message) {
@@ -4629,11 +4594,26 @@ async function resolveTaskCommandContext(command, optionsEnv, taskContext) {
4629
4594
  if (!ctx) {
4630
4595
  return null;
4631
4596
  }
4632
- const me = await ctx.client.me();
4597
+ const taskClientOptions = {
4598
+ token: ctx.token,
4599
+ onBehalfCreatorUsername: taskContext.creatorUsername,
4600
+ agentTaskToken: taskContext.taskToken,
4601
+ agentTaskId: taskContext.taskId,
4602
+ agentTaskAttempt: taskContext.attempt,
4603
+ };
4604
+ const client = (0, apiClient_1.createCliApiClient)({
4605
+ ...taskClientOptions,
4606
+ baseUrl: ctx.envConfig.apiBase,
4607
+ });
4608
+ const aiClient = (0, apiClient_1.createCliAiClient)({
4609
+ ...taskClientOptions,
4610
+ baseUrl: ctx.envConfig.aiBase,
4611
+ });
4612
+ const me = await client.me();
4633
4613
  if (!me.user) {
4634
4614
  throw new Error('task_context_user_missing');
4635
4615
  }
4636
- return { ...ctx, user: me.user };
4616
+ return { ...ctx, client, aiClient, user: me.user };
4637
4617
  }
4638
4618
  async function uploadTask(options = {}) {
4639
4619
  const taskContext = readTaskContextFile();
@@ -4772,6 +4752,7 @@ async function completeTask(options) {
4772
4752
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
4773
4753
  taskToken: taskContext.taskToken,
4774
4754
  error: 'remix_relation_missing',
4755
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)('upload_validation:remix_relation_missing', 'agent'),
4775
4756
  result: {
4776
4757
  appId: uploadResult.appId,
4777
4758
  appVersionId: uploadResult.appVersionId,
@@ -5035,6 +5016,7 @@ async function instrumentErrorTask(options) {
5035
5016
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
5036
5017
  taskToken: taskContext.taskToken,
5037
5018
  error: `instrument_error:${reason}`,
5019
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(`instrument_error:${reason}`, 'instrument'),
5038
5020
  result: {
5039
5021
  outcome: 'instrument_error',
5040
5022
  reason,
@@ -5058,6 +5040,7 @@ async function failTask(options) {
5058
5040
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
5059
5041
  taskToken: taskContext.taskToken,
5060
5042
  error: message,
5043
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'agent'),
5061
5044
  result: { failedBy: 'agent' },
5062
5045
  });
5063
5046
  (0, output_1.printSuccess)('Task marked failed. This was the final operation; send one short final status sentence and stop immediately.');