@playdrop/playdrop-cli 0.12.32 → 0.12.34

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 (30) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apiClient.js +16 -1
  3. package/dist/commandContext.js +3 -0
  4. package/dist/commands/upload.js +2 -0
  5. package/dist/commands/worker/runtime.d.ts +2 -0
  6. package/dist/commands/worker/runtime.js +14 -10
  7. package/dist/commands/worker.d.ts +9 -29
  8. package/dist/commands/worker.js +604 -660
  9. package/dist/index.js +6 -0
  10. package/dist/workspaceAuth.d.ts +1 -0
  11. package/dist/workspaceAuth.js +59 -0
  12. package/node_modules/@playdrop/api-client/dist/client.d.ts +2 -1
  13. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  14. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +2 -1
  15. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  16. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +11 -0
  17. package/node_modules/@playdrop/api-client/dist/index.d.ts +3 -2
  18. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  19. package/node_modules/@playdrop/api-client/dist/index.js +5 -0
  20. package/node_modules/@playdrop/config/client-meta.json +2 -2
  21. package/node_modules/@playdrop/types/dist/agent-task-terminal.d.ts +5 -0
  22. package/node_modules/@playdrop/types/dist/agent-task-terminal.d.ts.map +1 -0
  23. package/node_modules/@playdrop/types/dist/agent-task-terminal.js +117 -0
  24. package/node_modules/@playdrop/types/dist/api.d.ts +97 -103
  25. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  26. package/node_modules/@playdrop/types/dist/api.js +1 -1
  27. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  28. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  29. package/node_modules/@playdrop/types/dist/index.js +3 -1
  30. 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,9 @@ 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;
44
39
  exports.resolveCodexModel = resolveCodexModel;
45
40
  exports.resolveClaudeModel = resolveClaudeModel;
46
41
  exports.prepareCodexRunHomes = prepareCodexRunHomes;
@@ -76,6 +71,7 @@ exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
76
71
  exports.enforceInstrumentBrowserHygiene = enforceInstrumentBrowserHygiene;
77
72
  exports.startWorker = startWorker;
78
73
  exports.reportTask = reportTask;
74
+ exports.showTaskContext = showTaskContext;
79
75
  exports.reportCatalogueTask = reportCatalogueTask;
80
76
  exports.uploadTask = uploadTask;
81
77
  exports.claimSlugTask = claimSlugTask;
@@ -97,6 +93,7 @@ const node_os_1 = __importDefault(require("node:os"));
97
93
  const node_path_1 = __importDefault(require("node:path"));
98
94
  const node_process_1 = __importDefault(require("node:process"));
99
95
  const node_util_1 = require("node:util");
96
+ const apiClient_1 = require("../apiClient");
100
97
  const clientInfo_1 = require("../clientInfo");
101
98
  const commandContext_1 = require("../commandContext");
102
99
  const config_1 = require("../config");
@@ -136,15 +133,13 @@ const CLAIM_BACKOFF_JITTER_MS = 500;
136
133
  const FAIL_REPORT_RETRY_DELAY_MS = 2000;
137
134
  const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
138
135
  const SLACK_API_BASE_URL = 'https://slack.com/api';
139
- const WORKER_WRAPPER_CONTRACT_VERSION = 1;
136
+ const WORKER_WRAPPER_CONTRACT_VERSION = 2;
140
137
  const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
141
- const PLAYDROP_PLUGIN_MANIFEST_REQUIRED_STARTUP_KINDS = WORKER_SUPPORTED_KINDS;
142
138
  const requireFromWorker = (0, node_module_1.createRequire)(__filename);
143
- const STAGED_PLAYDROP_PLUGIN_ROOT = '.playdrop/plugin';
144
- const STAGED_CODEX_SKILL_ROOT = '.agents/skills';
145
- const PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH = 'playdrop-worker-staging.json';
139
+ const AGENT_BUNDLE_MANIFEST_PATH = 'agent-bundle.json';
146
140
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES = 1000;
147
141
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_UNCOMPRESSED_BYTES = 20 * 1024 * 1024;
142
+ const ASSIGNMENT_WORKSPACE_ARCHIVE_MAX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
148
143
  const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
149
144
  const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
150
145
  const ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES = 22;
@@ -152,6 +147,7 @@ const ZIP_MAX_COMMENT_BYTES = 0xffff;
152
147
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
153
148
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
154
149
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
150
+ ['task', 'context'],
155
151
  ['task', 'report'],
156
152
  ['task', 'report-catalogue'],
157
153
  ['task', 'claim-slug'],
@@ -300,116 +296,162 @@ function normalizeAssignmentAgent(value) {
300
296
  }
301
297
  return value;
302
298
  }
303
- function normalizeAssignmentKind(value, task) {
304
- if (value !== 'NEW_GAME' && value !== 'REMIX_GAME' && value !== 'GAME_UPDATE' && value !== 'GAME_REVIEW' && value !== 'GAME_EVAL') {
305
- throw new Error('agent_task_assignment_kind_missing');
306
- }
307
- if (value !== task.kind) {
308
- throw new Error('agent_task_assignment_kind_mismatch');
309
- }
310
- return value;
311
- }
312
299
  function normalizeAssignmentTarget(value) {
313
300
  if (value !== 'FIRST_PARTY' && value !== 'PERSONAL') {
314
301
  throw new Error('agent_task_assignment_target_missing');
315
302
  }
316
303
  return value;
317
304
  }
318
- function resolveWorkerClaimTaskAssignment(claim) {
319
- if (!claim.task) {
320
- 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`);
321
312
  }
322
- const assignment = claim.taskAssignment;
323
- if (!assignment || typeof assignment !== 'object') {
324
- 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`);
325
315
  }
326
- const prompt = typeof assignment.prompt === 'string' ? assignment.prompt.trim() : '';
327
- if (!prompt) {
328
- throw new Error('agent_task_assignment_prompt_missing');
329
- }
330
- const taskId = parsePositiveInteger(assignment.taskId, 'agent_task_assignment_task_id_missing');
331
- if (taskId !== claim.task.id) {
332
- 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`);
333
319
  }
334
- const taskToken = typeof assignment.taskToken === 'string' ? assignment.taskToken.trim() : '';
335
- if (!taskToken) {
336
- 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`);
337
322
  }
338
- const model = typeof assignment.model === 'string' ? assignment.model.trim() : '';
339
- if (!model) {
340
- throw new Error('agent_task_assignment_model_missing');
341
- }
342
- const reasoningEffort = assignment.reasoningEffort === null || assignment.reasoningEffort === undefined
343
- ? null
344
- : typeof assignment.reasoningEffort === 'string'
345
- ? assignment.reasoningEffort.trim()
346
- : '';
347
- if (reasoningEffort !== null && (!reasoningEffort || reasoningEffort.length > 40 || /\s/.test(reasoningEffort))) {
348
- throw new Error('invalid_agent_task_reasoning_effort');
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');
349
328
  }
350
- const workspace = assignment.workspace && typeof assignment.workspace === 'object' && !Array.isArray(assignment.workspace)
351
- ? assignment.workspace
352
- : null;
353
- if (!workspace) {
354
- throw new Error('agent_task_assignment_workspace_missing');
329
+ if (claim.action !== 'run') {
330
+ return null;
355
331
  }
356
- const inputs = assignment.inputs && typeof assignment.inputs === 'object' && !Array.isArray(assignment.inputs)
357
- ? assignment.inputs
358
- : null;
359
- if (!inputs) {
360
- throw new Error('agent_task_assignment_inputs_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
+ }
361
421
  }
362
- const output = assignment.output && typeof assignment.output === 'object' && !Array.isArray(assignment.output)
363
- ? assignment.output
364
- : null;
365
- if (!output) {
366
- throw new Error('agent_task_assignment_output_missing');
422
+ const browser = assignment.agent.browser;
423
+ if (browser !== 'NONE' && browser !== 'CHROME' && browser !== 'PLAYWRIGHT') {
424
+ throw new Error('agent_task_assignment_browser_invalid');
367
425
  }
368
- const version = typeof output.version === 'string' ? output.version.trim() : '';
369
- if (!version) {
370
- throw new Error('agent_task_assignment_output_version_missing');
371
- }
372
- const pluginBundle = assignment.pluginBundle && typeof assignment.pluginBundle === 'object' && !Array.isArray(assignment.pluginBundle)
373
- ? assignment.pluginBundle
374
- : null;
375
- if (!pluginBundle) {
376
- 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');
377
429
  }
378
430
  return {
379
- taskId,
380
- taskToken,
381
- kind: normalizeAssignmentKind(assignment.kind, claim.task),
382
- target: normalizeAssignmentTarget(assignment.target),
383
- agent: normalizeAssignmentAgent(assignment.agent),
384
- model,
385
- reasoningEffort,
386
- attemptPlan: assignment.attemptPlan ?? null,
387
- selectedAttempt: assignment.selectedAttempt ?? null,
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
+ },
388
439
  workspace: {
389
- folderName: normalizeAssignmentWorkspaceFolder(workspace.folderName),
390
- files: normalizeAssignmentWorkspaceFiles(workspace.files),
440
+ folderName: normalizeAssignmentWorkspaceFolder(assignment.workspace.folderName),
441
+ archives,
442
+ files,
391
443
  },
392
- inputs: {
393
- request: {
394
- prompt: normalizeAssignmentRequest(inputs.request),
395
- },
396
- attachments: Array.isArray(inputs.attachments) ? inputs.attachments : [],
397
- baseSource: inputs.baseSource ?? null,
398
- remixSource: inputs.remixSource ?? null,
399
- reviewTarget: inputs.reviewTarget ?? null,
400
- evalTarget: inputs.evalTarget ?? null,
401
- allowedTemplateKeys: Array.isArray(inputs.allowedTemplateKeys)
402
- ? Array.from(new Set(inputs.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
403
- : [],
444
+ plugin: assignment.plugin,
445
+ request: {
446
+ prompt,
447
+ attachments,
404
448
  },
405
- pluginBundle,
406
- output: {
407
- appId: typeof output.appId === 'number' && Number.isInteger(output.appId) && output.appId > 0 ? output.appId : null,
408
- appName: typeof output.appName === 'string' && output.appName.trim() ? output.appName.trim() : null,
409
- version,
410
- releaseNoteRequired: output.releaseNoteRequired !== false,
449
+ agent: {
450
+ runtime: normalizeAssignmentAgent(assignment.agent.runtime),
451
+ model,
452
+ reasoningEffort,
453
+ browser,
411
454
  },
412
- prompt,
413
455
  };
414
456
  }
415
457
  function nextClaimBackoffMs(currentMs) {
@@ -458,73 +500,55 @@ async function pruneWorkerTaskWorkspaces(incomingWorkspaceDir, retention = expor
458
500
  }
459
501
  return removed;
460
502
  }
461
- function buildWorkerTaskContext(claim, assignment) {
462
- const task = claim.task;
463
- if (!task) {
464
- throw new Error('agent_task_claim_missing_task');
465
- }
466
- if (task.id !== assignment.taskId) {
467
- throw new Error('agent_task_context_task_id_mismatch');
468
- }
469
- if (task.kind !== assignment.kind) {
470
- throw new Error('agent_task_context_kind_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');
471
507
  }
472
- if (task.executionTarget !== assignment.target) {
473
- throw new Error('agent_task_context_target_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');
474
512
  }
475
- const creatorUsername = typeof task.creatorUsername === 'string' ? task.creatorUsername.trim() : '';
476
- 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;
477
523
  if (!creatorUsername || !outputVersion) {
478
524
  throw new Error('agent_task_context_invalid');
479
525
  }
480
- const taskToken = typeof assignment.taskToken === 'string' ? assignment.taskToken.trim() : '';
481
- if (!taskToken) {
482
- throw new Error('agent_task_context_token_missing');
483
- }
484
- const attempt = Number(task.attempts);
485
- if (!Number.isInteger(attempt) || attempt <= 0) {
486
- throw new Error('agent_task_context_attempt_missing');
487
- }
488
- const outputAppName = typeof assignment.output.appName === 'string' && assignment.output.appName.trim()
489
- ? assignment.output.appName.trim()
526
+ const remixSource = metadata.remixSource && typeof metadata.remixSource === 'object' && !Array.isArray(metadata.remixSource)
527
+ ? metadata.remixSource
490
528
  : null;
491
- const reviewTarget = assignment.inputs.reviewTarget && typeof assignment.inputs.reviewTarget === 'object'
492
- ? assignment.inputs.reviewTarget
529
+ const remixSourceRef = typeof remixSource?.ref === 'string' && remixSource.ref.trim()
530
+ ? remixSource.ref.trim()
493
531
  : null;
494
- let reviewAppVersionId = null;
495
- if (assignment.kind === 'GAME_REVIEW') {
496
- const parsedReviewAppVersionId = Number(reviewTarget?.appVersionId);
497
- if (!Number.isInteger(parsedReviewAppVersionId) || parsedReviewAppVersionId <= 0) {
498
- throw new Error('agent_task_context_review_target_missing');
499
- }
500
- reviewAppVersionId = parsedReviewAppVersionId;
501
- }
502
- const remixSource = assignment.inputs.remixSource && typeof assignment.inputs.remixSource === 'object'
503
- ? assignment.inputs.remixSource
532
+ const reviewTarget = metadata.reviewTarget && typeof metadata.reviewTarget === 'object' && !Array.isArray(metadata.reviewTarget)
533
+ ? metadata.reviewTarget
504
534
  : null;
505
- let remixSourceRef = null;
506
- if (assignment.kind === 'REMIX_GAME') {
507
- const parsedRemixSourceRef = typeof remixSource?.ref === 'string' ? remixSource.ref.trim() : '';
508
- if (!parsedRemixSourceRef) {
509
- throw new Error('agent_task_context_remix_source_missing');
510
- }
511
- remixSourceRef = parsedRemixSourceRef;
512
- }
513
- const allowedTemplateKeys = Array.isArray(assignment.inputs.allowedTemplateKeys)
514
- ? 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()))
515
541
  : [];
516
- const creatorRequest = assignment.inputs.request.prompt.trim();
517
- if (!creatorRequest) {
518
- throw new Error('agent_task_context_creator_request_missing');
519
- }
520
542
  return {
521
- taskId: task.id,
522
- attempt,
523
- taskToken,
524
- 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,
525
549
  creatorUsername,
526
- creatorRequest,
527
- target: assignment.target,
550
+ creatorRequest: input.prompt,
551
+ target: input.target,
528
552
  outputAppName,
529
553
  outputVersion,
530
554
  remixSourceRef,
@@ -532,12 +556,62 @@ function buildWorkerTaskContext(claim, assignment) {
532
556
  reviewAppVersionId,
533
557
  };
534
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
+ }
535
570
  function workerTaskContextPath(workspaceDir) {
536
571
  return node_path_1.default.join(workspaceDir, '.playdrop', 'task.json');
537
572
  }
538
573
  function workerTaskUploadResultPath(workspaceDir) {
539
574
  return node_path_1.default.join(workspaceDir, '.playdrop', 'task-upload-result.json');
540
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
+ }
541
615
  async function stageWorkerTaskContext(input) {
542
616
  const playdropDir = node_path_1.default.join(input.workspaceDir, '.playdrop');
543
617
  await (0, promises_1.mkdir)(playdropDir, { recursive: true });
@@ -546,18 +620,10 @@ async function stageWorkerTaskContext(input) {
546
620
  devPort: input.devPort,
547
621
  env: input.env,
548
622
  };
549
- await (0, promises_1.writeFile)(workerTaskContextPath(input.workspaceDir), JSON.stringify(taskFile, null, 2), 'utf8');
550
- await (0, promises_1.writeFile)(node_path_1.default.join(input.workspaceDir, '.playdrop.json'), JSON.stringify({
551
- ownerUsername: input.taskContext.creatorUsername,
552
- env: input.env,
553
- taskId: input.taskContext.taskId,
554
- taskAttempt: input.taskContext.attempt,
555
- devPort: input.devPort,
556
- taskToken: input.taskContext.taskToken,
557
- }, null, 2), 'utf8');
623
+ await (0, promises_1.writeFile)(workerTaskContextPath(input.workspaceDir), JSON.stringify(serializeWorkerTaskContext(taskFile), null, 2), 'utf8');
558
624
  }
559
625
  async function writeWorkerTaskContextFile(workspaceDir, taskContext) {
560
- 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');
561
627
  }
562
628
  function findWorkspaceTaskFile(startDir, basename) {
563
629
  let current = node_path_1.default.resolve(startDir);
@@ -576,67 +642,57 @@ function findWorkspaceTaskFile(startDir, basename) {
576
642
  function readTaskContextFile(startDir = node_process_1.default.cwd()) {
577
643
  const file = findWorkspaceTaskFile(startDir, 'task.json');
578
644
  const parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
579
- const taskId = Number(parsed.taskId);
580
- const attempt = Number(parsed.attempt);
581
- const taskToken = typeof parsed.taskToken === 'string' ? parsed.taskToken.trim() : '';
582
- const creatorUsername = typeof parsed.creatorUsername === 'string' ? parsed.creatorUsername.trim() : '';
583
- const env = typeof parsed.env === 'string' ? parsed.env.trim() : '';
584
- const creatorRequest = typeof parsed.creatorRequest === 'string' ? parsed.creatorRequest.trim() : '';
585
- const outputVersion = typeof parsed.outputVersion === 'string' ? parsed.outputVersion.trim() : '';
586
- const remixSourceRef = typeof parsed.remixSourceRef === 'string' && parsed.remixSourceRef.trim()
587
- ? parsed.remixSourceRef.trim()
645
+ const task = parsed.task && typeof parsed.task === 'object' && !Array.isArray(parsed.task)
646
+ ? parsed.task
588
647
  : null;
589
- const allowedTemplateKeys = Array.isArray(parsed.allowedTemplateKeys)
590
- ? Array.from(new Set(parsed.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
591
- : [];
592
- let reviewAppVersionId = null;
593
- if (parsed.reviewAppVersionId !== null && parsed.reviewAppVersionId !== undefined) {
594
- const parsedReviewAppVersionId = Number(parsed.reviewAppVersionId);
595
- if (Number.isInteger(parsedReviewAppVersionId) && parsedReviewAppVersionId > 0) {
596
- reviewAppVersionId = parsedReviewAppVersionId;
597
- }
598
- }
599
- const devPort = typeof parsed.devPort === 'number' ? parsed.devPort : Number.parseInt(String(parsed.devPort ?? ''), 10);
600
- 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
601
668
  || !Number.isInteger(attempt) || attempt <= 0
602
669
  || !taskToken
603
- || (parsed.kind !== 'NEW_GAME' && parsed.kind !== 'REMIX_GAME' && parsed.kind !== 'GAME_UPDATE' && parsed.kind !== 'GAME_REVIEW' && parsed.kind !== 'GAME_EVAL')
604
- || (parsed.target !== 'FIRST_PARTY' && parsed.target !== 'PERSONAL')
605
- || !creatorUsername
606
- || !creatorRequest
670
+ || !taskType
671
+ || !metadata
672
+ || (task?.executionTarget !== 'FIRST_PARTY' && task?.executionTarget !== 'PERSONAL')
673
+ || !prompt.trim()
607
674
  || !env
608
- || !outputVersion
609
- || (parsed.kind === 'REMIX_GAME' && !remixSourceRef)
610
- || (parsed.kind === 'GAME_REVIEW' && reviewAppVersionId === null)
611
675
  || !Number.isInteger(devPort) || devPort <= 0 || devPort > 65535) {
612
676
  throw new Error(`task_context_invalid:${file}`);
613
677
  }
614
- return {
615
- taskId,
616
- attempt,
617
- taskToken,
618
- kind: parsed.kind,
619
- creatorUsername,
620
- creatorRequest,
621
- target: parsed.target,
622
- outputAppName: typeof parsed.outputAppName === 'string' && parsed.outputAppName.trim() ? parsed.outputAppName.trim() : null,
623
- outputVersion,
624
- remixSourceRef,
625
- allowedTemplateKeys,
626
- reviewAppVersionId,
627
- devPort,
628
- env,
629
- };
630
- }
631
- function normalizeAssignmentRequest(value) {
632
- const request = value && typeof value === 'object' && !Array.isArray(value)
633
- ? value
634
- : null;
635
- const prompt = typeof request?.prompt === 'string' ? request.prompt.trim() : '';
636
- if (!prompt) {
637
- 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}`);
638
695
  }
639
- return prompt;
640
696
  }
641
697
  function hasAgentTaskUploadedArtifact(task) {
642
698
  return ((Number.isInteger(task.appId) && Number(task.appId) > 0)
@@ -829,41 +885,6 @@ function normalizeAssignmentWorkspaceRelativePath(value) {
829
885
  }
830
886
  return normalizedPath;
831
887
  }
832
- function normalizeAssignmentWorkspaceFile(value) {
833
- const file = value && typeof value === 'object' && !Array.isArray(value)
834
- ? value
835
- : null;
836
- if (!file) {
837
- throw new Error('agent_task_assignment_workspace_file_invalid');
838
- }
839
- const relativePath = normalizeAssignmentWorkspaceRelativePath(file.relativePath);
840
- const contentBase64 = typeof file.contentBase64 === 'string' ? file.contentBase64.trim() : '';
841
- if (!contentBase64 || contentBase64.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(contentBase64)) {
842
- throw new Error('agent_task_assignment_workspace_file_content_invalid');
843
- }
844
- const contentType = typeof file.contentType === 'string' && file.contentType.trim()
845
- ? file.contentType.trim()
846
- : undefined;
847
- return {
848
- relativePath,
849
- contentBase64,
850
- ...(contentType ? { contentType } : {}),
851
- };
852
- }
853
- function normalizeAssignmentWorkspaceFiles(value) {
854
- if (!Array.isArray(value)) {
855
- return [];
856
- }
857
- const seen = new Set();
858
- return value.map((entry) => {
859
- const normalized = normalizeAssignmentWorkspaceFile(entry);
860
- if (seen.has(normalized.relativePath)) {
861
- throw new Error('agent_task_assignment_workspace_file_duplicate');
862
- }
863
- seen.add(normalized.relativePath);
864
- return normalized;
865
- });
866
- }
867
888
  function resolveWorkspaceFileDestination(workspaceDir, relativePath) {
868
889
  const root = node_path_1.default.resolve(workspaceDir);
869
890
  const destination = node_path_1.default.resolve(root, ...relativePath.split('/'));
@@ -873,12 +894,31 @@ function resolveWorkspaceFileDestination(workspaceDir, relativePath) {
873
894
  }
874
895
  return destination;
875
896
  }
876
- async function stageAssignmentWorkspaceFiles(workspaceDir, files) {
877
- const normalizedFiles = normalizeAssignmentWorkspaceFiles(files);
878
- for (const file of normalizedFiles) {
879
- 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);
880
907
  await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
881
- 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);
882
922
  }
883
923
  }
884
924
  function isFile(candidatePath) {
@@ -889,146 +929,6 @@ function isFile(candidatePath) {
889
929
  return false;
890
930
  }
891
931
  }
892
- function normalizePlaydropPluginManifestPath(input) {
893
- if (typeof input.value !== 'string' || input.value.trim().length <= 0) {
894
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: file paths must be non-empty strings in ${input.manifestPath}`);
895
- }
896
- if (input.value.includes('\0') || input.value.includes('\\')) {
897
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
898
- }
899
- if (node_path_1.default.isAbsolute(input.value) || /^[A-Za-z]:[\\/]/.test(input.value)) {
900
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
901
- }
902
- const normalized = node_path_1.default.posix.normalize(input.value);
903
- if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
904
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
905
- }
906
- return normalized;
907
- }
908
- function readPlaydropPluginStagingManifest(pluginRoot) {
909
- const resolvedRoot = node_path_1.default.resolve(pluginRoot);
910
- const manifestPath = node_path_1.default.join(resolvedRoot, PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH);
911
- if (!isFile(manifestPath)) {
912
- throw new Error(`playdrop_plugin_staging_manifest_missing: expected ${PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH} under ${resolvedRoot}`);
913
- }
914
- let parsed;
915
- try {
916
- parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
917
- }
918
- catch (error) {
919
- throw new Error(`playdrop_plugin_staging_manifest_invalid_json:${manifestPath}:${error instanceof Error ? error.message : String(error)}`);
920
- }
921
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
922
- throw new Error(`playdrop_plugin_staging_manifest_invalid:${manifestPath}`);
923
- }
924
- const rawManifest = parsed;
925
- if (rawManifest.version !== 1) {
926
- throw new Error(`playdrop_plugin_staging_manifest_unsupported_version:${manifestPath}`);
927
- }
928
- if (!rawManifest.taskFiles || typeof rawManifest.taskFiles !== 'object' || Array.isArray(rawManifest.taskFiles)) {
929
- throw new Error(`playdrop_plugin_staging_manifest_incomplete: taskFiles missing in ${manifestPath}`);
930
- }
931
- const taskFiles = {};
932
- const rawTaskFiles = rawManifest.taskFiles;
933
- for (const kind of PLAYDROP_PLUGIN_MANIFEST_REQUIRED_STARTUP_KINDS) {
934
- const rawFiles = rawTaskFiles[kind];
935
- if (!Array.isArray(rawFiles) || rawFiles.length <= 0) {
936
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${kind}: non-empty taskFiles entry required in ${manifestPath}`);
937
- }
938
- const seen = new Set();
939
- const files = rawFiles.map((value) => normalizePlaydropPluginManifestPath({ manifestPath, kind, value }));
940
- for (const relativePath of files) {
941
- if (seen.has(relativePath)) {
942
- throw new Error(`playdrop_plugin_staging_manifest_duplicate_file:${kind}:${relativePath}`);
943
- }
944
- seen.add(relativePath);
945
- const source = node_path_1.default.resolve(resolvedRoot, ...relativePath.split('/'));
946
- const relativeToRoot = node_path_1.default.relative(resolvedRoot, source);
947
- if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
948
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${kind}:${relativePath}`);
949
- }
950
- if (!isFile(source)) {
951
- throw new Error(`playdrop_plugin_staging_manifest_file_missing:${kind}:${relativePath}`);
952
- }
953
- }
954
- taskFiles[kind] = files;
955
- }
956
- for (const kind of WORKER_SUPPORTED_KINDS) {
957
- if (taskFiles[kind]) {
958
- continue;
959
- }
960
- const rawFiles = rawTaskFiles[kind];
961
- if (rawFiles === undefined) {
962
- continue;
963
- }
964
- if (!Array.isArray(rawFiles) || rawFiles.length <= 0) {
965
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${kind}: non-empty taskFiles entry required in ${manifestPath}`);
966
- }
967
- const seen = new Set();
968
- const files = rawFiles.map((value) => normalizePlaydropPluginManifestPath({ manifestPath, kind, value }));
969
- for (const relativePath of files) {
970
- if (seen.has(relativePath)) {
971
- throw new Error(`playdrop_plugin_staging_manifest_duplicate_file:${kind}:${relativePath}`);
972
- }
973
- seen.add(relativePath);
974
- const source = node_path_1.default.resolve(resolvedRoot, ...relativePath.split('/'));
975
- const relativeToRoot = node_path_1.default.relative(resolvedRoot, source);
976
- if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
977
- throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${kind}:${relativePath}`);
978
- }
979
- if (!isFile(source)) {
980
- throw new Error(`playdrop_plugin_staging_manifest_file_missing:${kind}:${relativePath}`);
981
- }
982
- }
983
- taskFiles[kind] = files;
984
- }
985
- return {
986
- version: 1,
987
- taskFiles: taskFiles,
988
- };
989
- }
990
- async function stagePlaydropPluginReferences(input) {
991
- const pluginRoot = node_path_1.default.resolve(input.pluginRoot);
992
- if (!WORKER_SUPPORTED_KINDS.includes(input.kind)) {
993
- throw new Error(`unsupported_agent_task_kind:${input.kind}`);
994
- }
995
- const manifest = readPlaydropPluginStagingManifest(pluginRoot);
996
- const referenceFiles = manifest.taskFiles[input.kind];
997
- if (!Array.isArray(referenceFiles) || referenceFiles.length <= 0) {
998
- throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: non-empty taskFiles entry required`);
999
- }
1000
- const staged = [];
1001
- for (const relativePath of referenceFiles) {
1002
- const destinationRelativePath = node_path_1.default.join(STAGED_PLAYDROP_PLUGIN_ROOT, relativePath);
1003
- const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
1004
- await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1005
- await (0, promises_1.copyFile)(node_path_1.default.join(pluginRoot, relativePath), destination);
1006
- staged.push(destinationRelativePath.replace(/\\/g, '/'));
1007
- }
1008
- return staged;
1009
- }
1010
- async function stageCodexTaskSkills(input) {
1011
- const skillRoot = node_path_1.default.join(input.workspaceDir, STAGED_CODEX_SKILL_ROOT);
1012
- await (0, promises_1.rm)(skillRoot, { recursive: true, force: true });
1013
- const stagedSkills = [];
1014
- for (const stagedPluginPath of input.stagedPluginPaths) {
1015
- const normalized = stagedPluginPath.replace(/\\/g, '/');
1016
- const match = /^\.playdrop\/plugin\/skills\/([^/]+)\/SKILL\.md$/.exec(normalized);
1017
- if (!match?.[1]) {
1018
- continue;
1019
- }
1020
- const source = resolveWorkspaceFileDestination(input.workspaceDir, normalized);
1021
- const destinationRelativePath = `${STAGED_CODEX_SKILL_ROOT}/${match[1]}/SKILL.md`;
1022
- const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
1023
- await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1024
- await (0, promises_1.copyFile)(source, destination);
1025
- stagedSkills.push(destinationRelativePath);
1026
- }
1027
- if (stagedSkills.length <= 0) {
1028
- throw new Error('agent_task_codex_skills_missing');
1029
- }
1030
- return stagedSkills.sort();
1031
- }
1032
932
  function assertAssignmentPluginBundleExtractionBounds(buffer) {
1033
933
  if (buffer.length < ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES) {
1034
934
  throw new Error('agent_task_plugin_bundle_invalid_zip');
@@ -1094,15 +994,102 @@ function assertAssignmentPluginBundleExtractionBounds(buffer) {
1094
994
  throw new Error('agent_task_plugin_bundle_invalid_zip');
1095
995
  }
1096
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
+ }
1097
1077
  async function stageAssignmentPluginBundle(input) {
1098
1078
  const sha256 = typeof input.pluginBundle.sha256 === 'string' ? input.pluginBundle.sha256.trim() : '';
1079
+ const fileName = typeof input.pluginBundle.fileName === 'string' ? input.pluginBundle.fileName.trim() : '';
1099
1080
  const contentBase64 = typeof input.pluginBundle.contentBase64 === 'string' ? input.pluginBundle.contentBase64.trim() : '';
1100
- 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) {
1101
1088
  throw new Error('agent_task_plugin_bundle_invalid');
1102
1089
  }
1103
1090
  const buffer = Buffer.from(contentBase64, 'base64');
1104
- if (buffer.length <= 0) {
1105
- throw new Error('agent_task_plugin_bundle_empty');
1091
+ if (buffer.length !== sizeBytes) {
1092
+ throw new Error('agent_task_plugin_bundle_size_mismatch');
1106
1093
  }
1107
1094
  const actualSha = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
1108
1095
  if (actualSha !== sha256) {
@@ -1140,8 +1127,7 @@ async function stageAssignmentPluginBundle(input) {
1140
1127
  await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1141
1128
  await (0, promises_1.writeFile)(destination, Buffer.from(content));
1142
1129
  }
1143
- readPlaydropPluginStagingManifest(pluginRoot);
1144
- return pluginRoot;
1130
+ return await stageAgentBundleWorkspaceFiles(pluginRoot, input.workspaceDir);
1145
1131
  }
1146
1132
  function resolveDevPluginWorkingTree(input) {
1147
1133
  const workingTree = input.workingTree?.trim();
@@ -1157,7 +1143,7 @@ function resolveDevPluginWorkingTree(input) {
1157
1143
  throw new Error(`worker_dev_plugin_working_tree_requires_dev_environment:${input.env}`);
1158
1144
  }
1159
1145
  const resolved = node_path_1.default.resolve(workingTree);
1160
- readPlaydropPluginStagingManifest(resolved);
1146
+ readAgentBundleManifest(resolved);
1161
1147
  return resolved;
1162
1148
  }
1163
1149
  // A PlayDrop project root is the topmost directory holding a catalogue.json,
@@ -1570,6 +1556,7 @@ function buildWorkerCapabilities(input) {
1570
1556
  }
1571
1557
  function buildWorkerClaimBody(input) {
1572
1558
  return {
1559
+ protocolVersion: 2,
1573
1560
  workerKey: input.workerKey,
1574
1561
  capabilities: {
1575
1562
  ...input.capabilities,
@@ -1577,89 +1564,43 @@ function buildWorkerClaimBody(input) {
1577
1564
  },
1578
1565
  };
1579
1566
  }
1580
- function resolveWorkerBaseSource(value) {
1581
- if (!value) {
1582
- throw new Error('agent_task_assignment_missing_base_source');
1583
- }
1584
- if (typeof value.appName !== 'string' || !value.appName.trim()) {
1585
- throw new Error('agent_task_base_app_name_missing');
1586
- }
1587
- if (typeof value.version !== 'string' || !value.version.trim()) {
1588
- throw new Error('agent_task_base_app_version_missing');
1589
- }
1590
- if (typeof value.appId !== 'number' || !Number.isInteger(value.appId) || value.appId <= 0) {
1591
- throw new Error('agent_task_base_app_id_missing');
1592
- }
1593
- if (typeof value.appVersionId !== 'number' || !Number.isInteger(value.appVersionId) || value.appVersionId <= 0) {
1594
- throw new Error('agent_task_base_app_version_id_missing');
1595
- }
1596
- return {
1597
- appName: value.appName.trim(),
1598
- version: value.version.trim(),
1599
- appId: value.appId,
1600
- appVersionId: value.appVersionId,
1601
- };
1602
- }
1603
- // Extracts a zip archive into targetDir using fflate (already a CLI
1604
- // dependency; see src/commands/create.ts). Entries that resolve outside the
1605
- // target directory throw instead of being skipped: a base source archive with
1606
- // 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.
1607
1569
  function extractZipArchive(zipBuffer, targetDir) {
1608
1570
  const files = (0, fflate_1.unzipSync)(zipBuffer);
1609
1571
  const names = Object.keys(files);
1610
1572
  if (names.length === 0) {
1611
- 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}`);
1612
1577
  }
1613
1578
  const resolvedTarget = node_path_1.default.resolve(targetDir);
1614
1579
  const written = [];
1580
+ let uncompressedBytes = 0;
1615
1581
  for (const originalName of names) {
1616
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
+ }
1617
1586
  const destination = node_path_1.default.resolve(resolvedTarget, name);
1618
1587
  if (destination !== resolvedTarget && !destination.startsWith(resolvedTarget + node_path_1.default.sep)) {
1619
- 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}`);
1620
1589
  }
1621
1590
  if (name.endsWith('/')) {
1622
1591
  (0, node_fs_1.mkdirSync)(destination, { recursive: true });
1623
1592
  continue;
1624
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
+ }
1625
1598
  (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(destination), { recursive: true });
1626
1599
  (0, node_fs_1.writeFileSync)(destination, Buffer.from(files[originalName]));
1627
1600
  written.push(name);
1628
1601
  }
1629
1602
  return written;
1630
1603
  }
1631
- // Reads the version string of the named app from the catalogue.json at the
1632
- // project root. Used for the staged base version and the version-bump guard.
1633
- function readWorkerProjectVersion(projectDir, appName) {
1634
- const cataloguePath = node_path_1.default.join(projectDir, 'catalogue.json');
1635
- let raw;
1636
- try {
1637
- raw = (0, node_fs_1.readFileSync)(cataloguePath, 'utf8');
1638
- }
1639
- catch (error) {
1640
- throw new Error(`agent_task_catalogue_unreadable: could not read ${cataloguePath} (${error instanceof Error ? error.message : String(error)}).`);
1641
- }
1642
- let parsed;
1643
- try {
1644
- parsed = JSON.parse(raw);
1645
- }
1646
- catch {
1647
- throw new Error(`agent_task_catalogue_invalid_json: ${cataloguePath} is not valid JSON.`);
1648
- }
1649
- const apps = parsed?.apps;
1650
- if (!Array.isArray(apps)) {
1651
- throw new Error(`agent_task_catalogue_apps_missing: ${cataloguePath} has no "apps" array.`);
1652
- }
1653
- const entry = apps.find((candidate) => Boolean(candidate) && typeof candidate === 'object' && candidate.name === appName);
1654
- if (!entry) {
1655
- throw new Error(`agent_task_catalogue_app_missing: no app named "${appName}" in ${cataloguePath}.`);
1656
- }
1657
- const version = typeof entry.version === 'string' ? entry.version.trim() : '';
1658
- if (!version) {
1659
- throw new Error(`agent_task_catalogue_version_missing: app "${appName}" in ${cataloguePath} has no version string.`);
1660
- }
1661
- return version;
1662
- }
1663
1604
  function normalizePreviewString(value) {
1664
1605
  const normalized = typeof value === 'string' ? value.trim() : '';
1665
1606
  return normalized || undefined;
@@ -1842,30 +1783,9 @@ async function drainWorkerEventQueue(input) {
1842
1783
  (0, node_fs_1.rmSync)(filePath, { force: true });
1843
1784
  }
1844
1785
  }
1845
- // Pre-publish guard for GAME_UPDATE tasks: the server rejects a duplicate
1846
- // version anyway, but failing here gives the task a precise error code.
1847
- function assertWorkerProjectVersionBumped(input) {
1848
- if (input.projectVersion === input.baseVersion) {
1849
- 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.`);
1850
- }
1851
- }
1852
- async function removeStaleWorkerControlFilesFromBaseSource(stagingDir) {
1786
+ async function removeStaleWorkerControlFiles(stagingDir) {
1853
1787
  await (0, promises_1.rm)(node_path_1.default.join(stagingDir, '.playdrop.json'), { force: true });
1854
1788
  }
1855
- // Downloads the base app source archive and unzips it into the workspace so
1856
- // the project root (catalogue.json) lands inside the workspace before Codex
1857
- // starts. Returns the staged project root and its catalogue version string.
1858
- async function stageWorkerBaseAppSource(input) {
1859
- const { blob } = await input.client.downloadAppSource(input.creatorUsername, input.baseApp.appName, input.baseApp.version);
1860
- const buffer = Buffer.from(await blob.arrayBuffer());
1861
- const stagingDir = node_path_1.default.join(input.workspaceDir, input.baseApp.appName);
1862
- (0, node_fs_1.mkdirSync)(stagingDir, { recursive: true });
1863
- extractZipArchive(new Uint8Array(buffer), stagingDir);
1864
- await removeStaleWorkerControlFilesFromBaseSource(stagingDir);
1865
- const stagedProjectDir = discoverWorkerProjectRoot(stagingDir);
1866
- const stagedBaseVersion = readWorkerProjectVersion(stagedProjectDir, input.baseApp.appName);
1867
- return { stagedProjectDir, stagedBaseVersion };
1868
- }
1869
1789
  function resolveCodexModel(model, reasoningEffortOverride) {
1870
1790
  const normalized = model.trim();
1871
1791
  if (!normalized) {
@@ -2011,6 +1931,21 @@ async function runCodex(input) {
2011
1931
  workerUsername: input.workerUsername,
2012
1932
  envName: input.envName,
2013
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
+ }
2014
1949
  let observedCodexThreadId = null;
2015
1950
  let codexThreadProbe = '';
2016
1951
  const result = await (0, runtime_1.runLoggedProcess)({
@@ -2021,11 +1956,13 @@ async function runCodex(input) {
2021
1956
  reasoningEffort: input.codexModel.reasoningEffort,
2022
1957
  networkAccess: (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_AGENT_NETWORK_ACCESS', false),
2023
1958
  sandboxMode: (0, runtime_1.readCodexSandboxMode)(),
1959
+ imagePaths: input.imagePaths,
2024
1960
  }),
2025
1961
  cwd: input.workspaceDir,
2026
1962
  env: (0, runtime_1.buildWorkerChildEnv)({
2027
1963
  binDir: input.binDir,
2028
1964
  taskId: input.taskId,
1965
+ taskToken: input.taskToken,
2029
1966
  attempt: input.attempt,
2030
1967
  envName: input.envName,
2031
1968
  eventDir: input.eventDir,
@@ -2106,6 +2043,7 @@ async function runClaude(input) {
2106
2043
  env: (0, runtime_1.buildWorkerChildEnv)({
2107
2044
  binDir: input.binDir,
2108
2045
  taskId: input.taskId,
2046
+ taskToken: input.taskToken,
2109
2047
  attempt: input.attempt,
2110
2048
  envName: input.envName,
2111
2049
  eventDir: input.eventDir,
@@ -2113,7 +2051,23 @@ async function runClaude(input) {
2113
2051
  playdropConfigPath,
2114
2052
  playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
2115
2053
  }),
2116
- 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`,
2117
2071
  timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
2118
2072
  maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
2119
2073
  onTranscriptChunks: input.onTranscriptChunks,
@@ -2163,6 +2117,13 @@ function buildAgentRunResult(result) {
2163
2117
  tokensUsed: result.tokensUsed,
2164
2118
  };
2165
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
+ }
2166
2127
  function buildSupervisorFailureRunResult(message) {
2167
2128
  return {
2168
2129
  exitCode: null,
@@ -2183,22 +2144,33 @@ function buildSupervisorFailureRunResult(message) {
2183
2144
  tokensUsed: null,
2184
2145
  };
2185
2146
  }
2186
- function stripStagedPlaydropPluginPrefix(value) {
2187
- const normalized = value.trim().replace(/\\/g, '/');
2188
- const pluginPrefix = `${STAGED_PLAYDROP_PLUGIN_ROOT}/`;
2189
- if (normalized.startsWith(pluginPrefix)) {
2190
- return normalized.slice(pluginPrefix.length);
2147
+ function listPluginSkillPaths(pluginRoot) {
2148
+ if (!pluginRoot) {
2149
+ return [];
2191
2150
  }
2192
- return normalized;
2193
- }
2194
- function normalizeTelemetrySkillPaths(stagedPaths) {
2195
- 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();
2196
2169
  }
2197
2170
  function collectObservedPlaydropSkillPaths(output, availablePaths) {
2198
2171
  const observed = new Set();
2199
2172
  for (const availablePath of availablePaths) {
2200
- if (output.includes(availablePath)
2201
- || output.includes(`${STAGED_PLAYDROP_PLUGIN_ROOT}/${availablePath}`)) {
2173
+ if (output.includes(availablePath)) {
2202
2174
  observed.add(availablePath);
2203
2175
  }
2204
2176
  }
@@ -2219,8 +2191,8 @@ async function recordAgentRunTelemetry(input) {
2219
2191
  workerKey: input.workerKey,
2220
2192
  leaseToken: input.leaseToken,
2221
2193
  attempt: input.task.attempts,
2222
- agent: input.assignment.agent,
2223
- requestedModel: input.assignment.model,
2194
+ agent: input.assignment.agent.runtime,
2195
+ requestedModel: input.assignment.agent.model,
2224
2196
  resolvedRuntimeModel: input.resolvedRuntimeModel,
2225
2197
  reasoningEffort: input.reasoningEffort,
2226
2198
  status: resolveTelemetryStatus(input.status),
@@ -2240,6 +2212,11 @@ async function recordAgentRunTelemetry(input) {
2240
2212
  observedSkillPaths: [...observedSkillPaths].sort(),
2241
2213
  startedAt: input.startedAt.toISOString(),
2242
2214
  completedAt: input.completedAt.toISOString(),
2215
+ ...(input.status === 'FAILED'
2216
+ ? {
2217
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(agentFailureRawError(input.result), 'agent'),
2218
+ }
2219
+ : {}),
2243
2220
  });
2244
2221
  }
2245
2222
  function providerNameForAgent(agent) {
@@ -2713,14 +2690,6 @@ function buildWorkerSetupActions(input) {
2713
2690
  message: 'The PlayDrop CLI package could not load playwright-core from its installed dependencies.',
2714
2691
  });
2715
2692
  }
2716
- if (reasons.has('playdrop_plugin_staging_manifest_missing')) {
2717
- actions.push({
2718
- reason: 'playdrop_plugin_staging_manifest_missing',
2719
- label: 'Install or update the PlayDrop agent plugin',
2720
- command: 'playdrop agents status --json',
2721
- message: 'Run the CLI-reported nextAction.command for a supported local agent so playdrop-worker-staging.json is available.',
2722
- });
2723
- }
2724
2693
  if (reasons.has('agent_cli_not_found')) {
2725
2694
  actions.push({
2726
2695
  reason: 'agent_cli_not_found',
@@ -3591,8 +3560,14 @@ async function setupWorker(options = {}) {
3591
3560
  }, { env: options.env });
3592
3561
  }
3593
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
+ };
3594
3569
  try {
3595
- await client.workerFailAgentTask(taskId, body);
3570
+ await client.workerFailAgentTask(taskId, requestBody);
3596
3571
  }
3597
3572
  catch (firstError) {
3598
3573
  if (isAgentTaskNotRunningError(firstError)) {
@@ -3600,7 +3575,7 @@ async function failTaskWithRetry(client, taskId, body) {
3600
3575
  }
3601
3576
  await sleep(FAIL_REPORT_RETRY_DELAY_MS);
3602
3577
  try {
3603
- await client.workerFailAgentTask(taskId, body);
3578
+ await client.workerFailAgentTask(taskId, requestBody);
3604
3579
  }
3605
3580
  catch (secondError) {
3606
3581
  if (isAgentTaskNotRunningError(secondError)) {
@@ -3620,13 +3595,24 @@ async function fetchTaskDetail(client, target, taskId) {
3620
3595
  return await client.getAgentTask(taskId);
3621
3596
  }
3622
3597
  function instrumentTaskChromeTitleMatchers(assignment) {
3623
- const targets = assignment.kind === 'GAME_REVIEW'
3624
- ? [assignment.inputs.reviewTarget]
3625
- : assignment.kind === 'GAME_EVAL'
3626
- ? 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
3627
3610
  : [];
3628
3611
  const matchers = new Set();
3629
- 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;
3630
3616
  if (!target)
3631
3617
  continue;
3632
3618
  for (const value of [target.appDisplayName, target.appName]) {
@@ -3719,7 +3705,7 @@ on run matchers
3719
3705
  end run
3720
3706
  `;
3721
3707
  async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3722
- if (assignment.kind !== 'GAME_REVIEW' && assignment.kind !== 'GAME_EVAL') {
3708
+ if (assignment.agent.browser === 'NONE') {
3723
3709
  return { remainingTaskTabs: 0, blankWindows: 0 };
3724
3710
  }
3725
3711
  if ((options.platform ?? node_process_1.default.platform) !== 'darwin') {
@@ -3927,11 +3913,19 @@ async function startWorker(options = {}) {
3927
3913
  });
3928
3914
  }, HEARTBEAT_INTERVAL_MS);
3929
3915
  const runClaimedTask = async (claim, devPort) => {
3930
- const task = claim.task;
3931
- if (!task) {
3932
- 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');
3933
3919
  }
3934
- 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() : '';
3935
3929
  if (!leaseToken) {
3936
3930
  throw new Error('agent_task_claim_missing_lease_token');
3937
3931
  }
@@ -3946,7 +3940,7 @@ async function startWorker(options = {}) {
3946
3940
  let workspaceDir = null;
3947
3941
  let availableSkillPaths = [];
3948
3942
  const observedSkillPaths = new Set();
3949
- let assignment = null;
3943
+ const assignment = claimedAssignment;
3950
3944
  let resolvedRuntimeModel = task.model;
3951
3945
  let reasoningEffort = null;
3952
3946
  let agentStartedAt = null;
@@ -3955,9 +3949,6 @@ async function startWorker(options = {}) {
3955
3949
  let telemetryReported = false;
3956
3950
  let heartbeatTransientFailures = 0;
3957
3951
  const reportTelemetry = async (status, setupError) => {
3958
- if (!assignment) {
3959
- return;
3960
- }
3961
3952
  const completedAt = agentCompletedAt ?? new Date();
3962
3953
  await recordAgentRunTelemetry({
3963
3954
  client,
@@ -3977,24 +3968,19 @@ async function startWorker(options = {}) {
3977
3968
  telemetryReported = true;
3978
3969
  };
3979
3970
  try {
3980
- assignment = resolveWorkerClaimTaskAssignment(claim);
3981
- if (!assignment) {
3982
- throw new Error('agent_task_claim_missing_task_assignment');
3983
- }
3984
- const codexModel = assignment.agent === 'CODEX'
3985
- ? resolveCodexModel(assignment.model, assignment.reasoningEffort)
3971
+ const codexModel = assignment.agent.runtime === 'CODEX'
3972
+ ? resolveCodexModel(assignment.agent.model, assignment.agent.reasoningEffort)
3986
3973
  : null;
3987
- const claudeModel = assignment.agent === 'CLAUDE_CODE'
3988
- ? resolveClaudeModel(assignment.model, assignment.reasoningEffort)
3974
+ const claudeModel = assignment.agent.runtime === 'CLAUDE_CODE'
3975
+ ? resolveClaudeModel(assignment.agent.model, assignment.agent.reasoningEffort)
3989
3976
  : null;
3990
- resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.model;
3977
+ resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.agent.model;
3991
3978
  reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
3992
- const taskContext = buildWorkerTaskContext(claim, assignment);
3993
3979
  workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
3994
3980
  await pruneWorkerTaskWorkspaces(workspaceDir);
3995
3981
  await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
3996
3982
  await (0, promises_1.mkdir)(workspaceDir, { recursive: true });
3997
- const eventDir = node_path_1.default.join(workspaceDir, '.playdrop-task-events');
3983
+ const eventDir = node_path_1.default.join(workspaceDir, '.playdrop', 'task-events');
3998
3984
  await (0, promises_1.mkdir)(eventDir, { recursive: true });
3999
3985
  await stageWorkerTaskContext({
4000
3986
  workspaceDir,
@@ -4039,52 +4025,16 @@ async function startWorker(options = {}) {
4039
4025
  appendObservedSkillPathsFromTranscript(chunks);
4040
4026
  await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
4041
4027
  };
4042
- await client.workerCreateAgentTaskEvent(task.id, {
4043
- workerKey,
4044
- leaseToken,
4045
- kind: 'progress',
4046
- phase: 'setup',
4047
- message: 'Preparing worker workspace',
4048
- pct: 2,
4049
- });
4050
- const assignmentPluginRoot = devPluginWorkingTree ?? await stageAssignmentPluginBundle({
4051
- workspaceDir,
4052
- pluginBundle: assignment.pluginBundle,
4053
- });
4054
- const stagedPluginPaths = await stagePlaydropPluginReferences({
4055
- workspaceDir,
4056
- pluginRoot: assignmentPluginRoot,
4057
- kind: task.kind,
4058
- });
4059
- if (assignment.agent === 'CODEX') {
4060
- await stageCodexTaskSkills({ workspaceDir, stagedPluginPaths });
4061
- }
4062
- availableSkillPaths = normalizeTelemetrySkillPaths(stagedPluginPaths);
4063
- await client.workerCreateAgentTaskEvent(task.id, {
4064
- workerKey,
4065
- leaseToken,
4066
- kind: 'progress',
4067
- phase: 'setup',
4068
- message: devPluginWorkingTree
4069
- ? 'Staged PlayDrop plugin references from explicit dev working tree'
4070
- : 'Staged PlayDrop plugin references from server task bundle',
4071
- pct: 3,
4072
- payload: {
4073
- pluginSource: devPluginWorkingTree ? 'DEV_WORKING_TREE' : 'SERVER_TASK_BUNDLE',
4074
- bundleSha: assignment.pluginBundle.sha256,
4075
- },
4076
- });
4077
- if (assignment.workspace.files.length > 0) {
4078
- await client.workerCreateAgentTaskEvent(task.id, {
4079
- workerKey,
4080
- leaseToken,
4081
- kind: 'progress',
4082
- phase: 'setup',
4083
- message: `Staging ${assignment.workspace.files.length} server-provided workspace file${assignment.workspace.files.length === 1 ? '' : 's'}`,
4084
- pct: 3,
4085
- });
4086
- await stageAssignmentWorkspaceFiles(workspaceDir, assignment.workspace.files);
4087
- }
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);
4088
4038
  heartbeatTimer = setInterval(() => {
4089
4039
  client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
4090
4040
  .then((heartbeat) => {
@@ -4143,57 +4093,18 @@ async function startWorker(options = {}) {
4143
4093
  handleEventDrainFailure(error);
4144
4094
  });
4145
4095
  }, 1000);
4146
- const prompt = assignment.prompt;
4147
- if (task.kind === 'GAME_UPDATE') {
4148
- const baseSourcePayload = assignment.inputs.baseSource;
4149
- const baseSource = resolveWorkerBaseSource(baseSourcePayload);
4150
- await client.workerCreateAgentTaskEvent(task.id, {
4151
- workerKey,
4152
- leaseToken,
4153
- kind: 'progress',
4154
- phase: 'setup',
4155
- message: `Staging base source ${baseSource.appName} version ${baseSource.version} into the workspace`,
4156
- pct: 4,
4157
- });
4158
- await stageWorkerBaseAppSource({
4159
- client,
4160
- workspaceDir,
4161
- creatorUsername: baseSourcePayload.creatorUsername,
4162
- baseApp: baseSource,
4163
- });
4164
- }
4165
- else if (task.kind === 'NEW_GAME' || task.kind === 'REMIX_GAME') {
4166
- if (assignment.inputs.baseSource !== null) {
4167
- throw new Error('agent_task_assignment_unexpected_base_source');
4168
- }
4169
- if (task.kind === 'REMIX_GAME' && !assignment.inputs.remixSource) {
4170
- throw new Error('agent_task_assignment_remix_source_missing');
4171
- }
4172
- }
4173
- else if (task.kind === 'GAME_REVIEW') {
4174
- if (assignment.inputs.baseSource !== null) {
4175
- throw new Error('agent_task_assignment_unexpected_base_source');
4176
- }
4177
- if (!assignment.inputs.reviewTarget) {
4178
- throw new Error('agent_task_assignment_review_target_missing');
4179
- }
4180
- }
4181
- else if (task.kind === 'GAME_EVAL') {
4182
- if (assignment.inputs.baseSource !== null) {
4183
- throw new Error('agent_task_assignment_unexpected_base_source');
4184
- }
4185
- if (!assignment.inputs.evalTarget) {
4186
- throw new Error('agent_task_assignment_eval_target_missing');
4187
- }
4188
- }
4189
- else {
4190
- throw new Error(`unsupported_agent_task_kind:${task.kind}`);
4191
- }
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
+ }));
4192
4103
  if (shuttingDown) {
4193
4104
  throw new Error('worker_shutdown');
4194
4105
  }
4195
4106
  agentStartedAt = new Date();
4196
- if (assignment.agent === 'CODEX') {
4107
+ if (assignment.agent.runtime === 'CODEX') {
4197
4108
  agentResult = await runCodex({
4198
4109
  workspaceDir,
4199
4110
  binDir,
@@ -4202,9 +4113,12 @@ async function startWorker(options = {}) {
4202
4113
  envName: env,
4203
4114
  workerUsername: username,
4204
4115
  taskId: task.id,
4116
+ taskToken: assignment.task.token,
4205
4117
  attempt: taskContext.attempt,
4206
4118
  devPort,
4207
- codexModel: codexModel ?? resolveCodexModel(assignment.model),
4119
+ codexModel: codexModel ?? resolveCodexModel(assignment.agent.model),
4120
+ taskPluginRoot: assignmentPluginRoot,
4121
+ imagePaths: imageAttachments.map((attachment) => attachment.path),
4208
4122
  onTranscriptChunks: async (chunks) => {
4209
4123
  await appendTranscriptChunks(chunks);
4210
4124
  },
@@ -4213,21 +4127,23 @@ async function startWorker(options = {}) {
4213
4127
  },
4214
4128
  });
4215
4129
  }
4216
- else if (assignment.agent === 'CLAUDE_CODE') {
4130
+ else if (assignment.agent.runtime === 'CLAUDE_CODE') {
4217
4131
  agentResult = await runClaude({
4218
4132
  workspaceDir,
4219
4133
  binDir,
4220
4134
  eventDir,
4221
4135
  prompt,
4222
- enableChrome: false,
4223
- enablePlaywrightMcp: task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL',
4136
+ enableChrome: assignment.agent.browser === 'CHROME',
4137
+ enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4224
4138
  envName: env,
4225
4139
  workerUsername: username,
4226
4140
  taskId: task.id,
4141
+ taskToken: assignment.task.token,
4227
4142
  attempt: taskContext.attempt,
4228
4143
  devPort,
4229
- claudeModel: claudeModel ?? resolveClaudeModel(assignment.model),
4144
+ claudeModel: claudeModel ?? resolveClaudeModel(assignment.agent.model),
4230
4145
  taskPluginRoot: assignmentPluginRoot,
4146
+ imageAttachments,
4231
4147
  onTranscriptChunks: async (chunks) => {
4232
4148
  await appendTranscriptChunks(chunks);
4233
4149
  },
@@ -4236,17 +4152,11 @@ async function startWorker(options = {}) {
4236
4152
  },
4237
4153
  });
4238
4154
  }
4239
- else if (assignment.agent === 'CURSOR_COMPOSER') {
4240
- const assignmentAgent = assignment.agent;
4241
- const assignmentModel = assignment.model;
4242
- const plannedAttemptIndex = assignment.attemptPlan?.options.findIndex((option) => option.agent === assignmentAgent && option.model === assignmentModel);
4243
- const attemptIndex = typeof plannedAttemptIndex === 'number' && plannedAttemptIndex >= 0
4244
- ? plannedAttemptIndex
4245
- : 0;
4155
+ else if (assignment.agent.runtime === 'CURSOR_COMPOSER') {
4246
4156
  await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4247
4157
  workerKey,
4248
4158
  leaseToken,
4249
- attemptIndex,
4159
+ attemptIndex: 0,
4250
4160
  reason: 'unavailable_runtime',
4251
4161
  message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4252
4162
  });
@@ -4254,7 +4164,7 @@ async function startWorker(options = {}) {
4254
4164
  throw new Error('worker_cursor_composer_runner_not_configured');
4255
4165
  }
4256
4166
  else {
4257
- throw new Error(`unsupported_worker_agent:${assignment.agent}`);
4167
+ throw new Error(`unsupported_worker_agent:${assignment.agent.runtime}`);
4258
4168
  }
4259
4169
  agentCompletedAt = new Date();
4260
4170
  await queueEventDrain().catch((error) => {
@@ -4313,8 +4223,8 @@ async function startWorker(options = {}) {
4313
4223
  }
4314
4224
  else {
4315
4225
  retainWorkspace = true;
4316
- const failureCode = buildAgentFailureCode(assignment.agent, completedAgentResult, {
4317
- model: assignment.model,
4226
+ const failureCode = buildAgentFailureCode(assignment.agent.runtime, completedAgentResult, {
4227
+ model: assignment.agent.model,
4318
4228
  reasoningEffort,
4319
4229
  });
4320
4230
  await client.workerCreateAgentTaskEvent(task.id, {
@@ -4330,6 +4240,7 @@ async function startWorker(options = {}) {
4330
4240
  workerKey,
4331
4241
  leaseToken,
4332
4242
  error: failureCode,
4243
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(agentFailureRawError(completedAgentResult), 'agent'),
4333
4244
  result: agentRunResult,
4334
4245
  });
4335
4246
  await reportTelemetry('FAILED');
@@ -4416,6 +4327,7 @@ async function startWorker(options = {}) {
4416
4327
  workerKey,
4417
4328
  leaseToken,
4418
4329
  error: normalizeWorkerFailureErrorCode(message),
4330
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'worker'),
4419
4331
  result: { workerError: message },
4420
4332
  });
4421
4333
  }
@@ -4438,9 +4350,8 @@ async function startWorker(options = {}) {
4438
4350
  if (eventDrainTimer) {
4439
4351
  clearInterval(eventDrainTimer);
4440
4352
  }
4441
- if (assignment
4442
- && agentStartedAt
4443
- && (assignment.kind === 'GAME_REVIEW' || assignment.kind === 'GAME_EVAL')) {
4353
+ if (agentStartedAt
4354
+ && assignment.agent.browser !== 'NONE') {
4444
4355
  try {
4445
4356
  const hygiene = await enforceInstrumentBrowserHygiene(assignment);
4446
4357
  console.log(`Agent task ${task.id} browser hygiene passed: ${hygiene.remainingTaskTabs} task tabs, ${hygiene.blankWindows} blank window(s).`);
@@ -4514,8 +4425,7 @@ async function startWorker(options = {}) {
4514
4425
  claimBackoffMs = nextClaimBackoffMs(claimBackoffMs);
4515
4426
  continue;
4516
4427
  }
4517
- const task = claim.task;
4518
- if (!task) {
4428
+ if (claim.action !== 'run') {
4519
4429
  if (shuttingDown || quiescingForUpdate) {
4520
4430
  break;
4521
4431
  }
@@ -4525,18 +4435,19 @@ async function startWorker(options = {}) {
4525
4435
  await sleep(DEFAULT_POLL_INTERVAL_MS);
4526
4436
  continue;
4527
4437
  }
4528
- activeTaskIds.add(task.id);
4438
+ const taskId = claim.assignment.task.id;
4439
+ activeTaskIds.add(taskId);
4529
4440
  activeDevPorts.add(devPort);
4530
4441
  const taskRun = runClaimedTask(claim, devPort)
4531
4442
  .catch((error) => {
4532
4443
  recordFatalTaskError(error);
4533
4444
  })
4534
4445
  .finally(() => {
4535
- activeTaskRuns.delete(task.id);
4536
- activeTaskIds.delete(task.id);
4446
+ activeTaskRuns.delete(taskId);
4447
+ activeTaskIds.delete(taskId);
4537
4448
  activeDevPorts.delete(devPort);
4538
4449
  });
4539
- activeTaskRuns.set(task.id, taskRun);
4450
+ activeTaskRuns.set(taskId, taskRun);
4540
4451
  if (options.once) {
4541
4452
  await taskRun;
4542
4453
  if (fatalTaskError) {
@@ -4633,6 +4544,21 @@ async function reportTask(options) {
4633
4544
  }
4634
4545
  resolveWorkerTaskStateForWrite();
4635
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
+ }
4636
4562
  async function reportCatalogueTask(options) {
4637
4563
  const message = options.message?.trim();
4638
4564
  if (!message) {
@@ -4668,11 +4594,26 @@ async function resolveTaskCommandContext(command, optionsEnv, taskContext) {
4668
4594
  if (!ctx) {
4669
4595
  return null;
4670
4596
  }
4671
- 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();
4672
4613
  if (!me.user) {
4673
4614
  throw new Error('task_context_user_missing');
4674
4615
  }
4675
- return { ...ctx, user: me.user };
4616
+ return { ...ctx, client, aiClient, user: me.user };
4676
4617
  }
4677
4618
  async function uploadTask(options = {}) {
4678
4619
  const taskContext = readTaskContextFile();
@@ -4811,6 +4752,7 @@ async function completeTask(options) {
4811
4752
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
4812
4753
  taskToken: taskContext.taskToken,
4813
4754
  error: 'remix_relation_missing',
4755
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)('upload_validation:remix_relation_missing', 'agent'),
4814
4756
  result: {
4815
4757
  appId: uploadResult.appId,
4816
4758
  appVersionId: uploadResult.appVersionId,
@@ -5074,6 +5016,7 @@ async function instrumentErrorTask(options) {
5074
5016
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
5075
5017
  taskToken: taskContext.taskToken,
5076
5018
  error: `instrument_error:${reason}`,
5019
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(`instrument_error:${reason}`, 'instrument'),
5077
5020
  result: {
5078
5021
  outcome: 'instrument_error',
5079
5022
  reason,
@@ -5097,6 +5040,7 @@ async function failTask(options) {
5097
5040
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
5098
5041
  taskToken: taskContext.taskToken,
5099
5042
  error: message,
5043
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(message, 'agent'),
5100
5044
  result: { failedBy: 'agent' },
5101
5045
  });
5102
5046
  (0, output_1.printSuccess)('Task marked failed. This was the final operation; send one short final status sentence and stop immediately.');