@playdrop/playdrop-cli 0.13.15 → 0.13.17

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 (29) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/staticHtml.js +1 -0
  3. package/dist/commandContext.js +1 -1
  4. package/dist/commands/captureListing.d.ts +43 -0
  5. package/dist/commands/captureListing.js +184 -5
  6. package/dist/commands/upload.d.ts +12 -1
  7. package/dist/commands/upload.js +97 -88
  8. package/dist/commands/whoami.js +10 -1
  9. package/dist/commands/worker/runtime.js +2 -0
  10. package/dist/commands/worker.d.ts +28 -1
  11. package/dist/commands/worker.js +376 -28
  12. package/dist/index.js +17 -2
  13. package/dist/listingPreflight.d.ts +2 -1
  14. package/dist/listingPreflight.js +49 -18
  15. package/dist/workerAppProject.d.ts +2 -0
  16. package/dist/workerAppProject.js +33 -0
  17. package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -2
  18. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  19. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +27 -1
  20. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  21. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +103 -0
  22. package/node_modules/@playdrop/api-client/dist/index.d.ts +4 -2
  23. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  24. package/node_modules/@playdrop/api-client/dist/index.js +10 -0
  25. package/node_modules/@playdrop/config/client-meta.json +2 -2
  26. package/node_modules/@playdrop/types/dist/api.d.ts +150 -2
  27. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  28. package/node_modules/@playdrop/types/dist/api.js +3 -1
  29. package/package.json +1 -1
@@ -7,6 +7,7 @@ exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALL
7
7
  exports.usesCreatorRecovery = usesCreatorRecovery;
8
8
  exports.buildCreatorRecoveryPrompt = buildCreatorRecoveryPrompt;
9
9
  exports.buildGameEvalRetryPrompt = buildGameEvalRetryPrompt;
10
+ exports.resolveInitialCreatorProgress = resolveInitialCreatorProgress;
10
11
  exports.allocateWorkerDevPort = allocateWorkerDevPort;
11
12
  exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
12
13
  exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
@@ -50,6 +51,7 @@ exports.removeAgentRunCredentials = removeAgentRunCredentials;
50
51
  exports.removeStaleAgentRunCredentials = removeStaleAgentRunCredentials;
51
52
  exports.mergeCreatorRecoveryResults = mergeCreatorRecoveryResults;
52
53
  exports.buildAgentFailureCode = buildAgentFailureCode;
54
+ exports.classifyAgentAvailabilityFailureReason = classifyAgentAvailabilityFailureReason;
53
55
  exports.describeAgentFailureForEvent = describeAgentFailureForEvent;
54
56
  exports.createLocalPlaydropShim = createLocalPlaydropShim;
55
57
  exports.parseCodexModelCatalog = parseCodexModelCatalog;
@@ -80,6 +82,8 @@ exports.assignmentRequiresRegisteredBundle = assignmentRequiresRegisteredBundle;
80
82
  exports.startWorker = startWorker;
81
83
  exports.reportTask = reportTask;
82
84
  exports.showTaskContext = showTaskContext;
85
+ exports.assertTaskAssetMaterialIsTransparentPng = assertTaskAssetMaterialIsTransparentPng;
86
+ exports.materialTask = materialTask;
83
87
  exports.reportCatalogueTask = reportCatalogueTask;
84
88
  exports.uploadTask = uploadTask;
85
89
  exports.claimSlugTask = claimSlugTask;
@@ -101,6 +105,7 @@ const node_os_1 = __importDefault(require("node:os"));
101
105
  const node_path_1 = __importDefault(require("node:path"));
102
106
  const node_process_1 = __importDefault(require("node:process"));
103
107
  const node_util_1 = require("node:util");
108
+ const sharp_1 = __importDefault(require("sharp"));
104
109
  const apiClient_1 = require("../apiClient");
105
110
  const clientInfo_1 = require("../clientInfo");
106
111
  const commandContext_1 = require("../commandContext");
@@ -196,6 +201,7 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
196
201
  ['task', 'context'],
197
202
  ['task', 'help'],
198
203
  ['task', 'report'],
204
+ ['task', 'material'],
199
205
  ['task', 'report-catalogue'],
200
206
  ['task', 'claim-slug'],
201
207
  ['task', 'upload'],
@@ -227,6 +233,33 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
227
233
  ['ai', 'generations'],
228
234
  ];
229
235
  exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = 'worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task claim-slug, task upload/done/fail, assigned app scaffolding, project validation/build/dev/check/capture, read-only help/catalogue/documentation lookup, and PlayDrop AI generation are permitted.';
236
+ function resolveInitialCreatorProgress(kind, creationRecovery = false) {
237
+ if (creationRecovery) {
238
+ return null;
239
+ }
240
+ if (kind === 'NEW_GAME') {
241
+ return {
242
+ kind: 'progress',
243
+ phase: 'setup',
244
+ current: 'Reviewing your game idea',
245
+ };
246
+ }
247
+ if (kind === 'REMIX_GAME') {
248
+ return {
249
+ kind: 'progress',
250
+ phase: 'setup',
251
+ current: 'Reviewing the source game and your idea',
252
+ };
253
+ }
254
+ if (kind === 'GAME_UPDATE') {
255
+ return {
256
+ kind: 'progress',
257
+ phase: 'setup',
258
+ current: 'Reviewing your game and requested change',
259
+ };
260
+ }
261
+ return null;
262
+ }
230
263
  function allocateWorkerDevPort(input) {
231
264
  const basePort = input.basePort ?? DEFAULT_WORKER_DEV_PORT_BASE;
232
265
  const maxParallelTasks = input.maxParallelTasks ?? DEFAULT_WORKER_MAX_PARALLEL_TASKS;
@@ -591,6 +624,13 @@ function buildWorkerTaskContext(input) {
591
624
  const outputAppName = typeof output?.appName === 'string' && output.appName.trim()
592
625
  ? output.appName.trim()
593
626
  : null;
627
+ const outputDisplayName = typeof output?.displayName === 'string' && output.displayName.trim()
628
+ ? output.displayName.trim()
629
+ : null;
630
+ const outputSubtitle = typeof output?.subtitle === 'string' && output.subtitle.trim()
631
+ ? output.subtitle.trim()
632
+ : null;
633
+ const outputPrimarySurface = normalizeWorkerInstrumentSurface(output?.primarySurface);
594
634
  if (!creatorUsername || !outputVersion) {
595
635
  throw new Error('agent_task_context_invalid');
596
636
  }
@@ -621,6 +661,9 @@ function buildWorkerTaskContext(input) {
621
661
  creatorRequest: input.prompt,
622
662
  target: input.target,
623
663
  outputAppName,
664
+ outputDisplayName,
665
+ outputSubtitle,
666
+ outputPrimarySurface,
624
667
  outputVersion,
625
668
  remixSourceRef,
626
669
  allowedTemplateKeys,
@@ -703,6 +746,9 @@ function workerTaskContextPath(workspaceDir) {
703
746
  function workerTaskUploadResultPath(workspaceDir) {
704
747
  return node_path_1.default.join(workspaceDir, '.playdrop', 'task-upload-result.json');
705
748
  }
749
+ function workerTaskMaterialsPath(workspaceDir) {
750
+ return node_path_1.default.join(workspaceDir, '.playdrop', 'task-materials.json');
751
+ }
706
752
  function serializeWorkerTaskContext(taskContext) {
707
753
  const playdrop = taskContext.metadata.playdrop;
708
754
  if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
@@ -720,6 +766,9 @@ function serializeWorkerTaskContext(taskContext) {
720
766
  output: {
721
767
  ...output,
722
768
  appName: taskContext.outputAppName,
769
+ displayName: taskContext.outputDisplayName,
770
+ subtitle: taskContext.outputSubtitle,
771
+ primarySurface: taskContext.outputPrimarySurface,
723
772
  },
724
773
  },
725
774
  };
@@ -887,6 +936,28 @@ function normalizeTaskClaimSlugDisplayName(value) {
887
936
  }
888
937
  return displayName;
889
938
  }
939
+ function normalizeTaskClaimSlugSubtitle(value) {
940
+ if (value === undefined) {
941
+ return null;
942
+ }
943
+ const subtitle = typeof value === 'string' ? value.trim() : '';
944
+ if (!subtitle || subtitle.length > 120) {
945
+ throw new Error('invalid_app_subtitle');
946
+ }
947
+ return subtitle;
948
+ }
949
+ function normalizeTaskClaimSlugPrimarySurface(value) {
950
+ if (value === undefined) {
951
+ return null;
952
+ }
953
+ const primarySurface = typeof value === 'string' ? value.trim().toUpperCase() : '';
954
+ if (primarySurface !== 'DESKTOP'
955
+ && primarySurface !== 'MOBILE_LANDSCAPE'
956
+ && primarySurface !== 'MOBILE_PORTRAIT') {
957
+ throw new Error('invalid_primary_surface');
958
+ }
959
+ return primarySurface;
960
+ }
890
961
  function readTaskNextStepsFile(filePath) {
891
962
  const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
892
963
  if (!normalizedPath) {
@@ -1447,11 +1518,7 @@ function resolveWorkerEventQueueDir() {
1447
1518
  const eventDir = node_process_1.default.env.PLAYDROP_WORKER_EVENT_DIR?.trim();
1448
1519
  return eventDir || null;
1449
1520
  }
1450
- function enqueueWorkerContextEvent(event) {
1451
- const eventDir = resolveWorkerEventQueueDir();
1452
- if (!eventDir) {
1453
- throw new Error('task_context_event_queue_missing');
1454
- }
1521
+ function enqueueWorkerEvent(eventDir, event) {
1455
1522
  (0, node_fs_1.mkdirSync)(eventDir, { recursive: true });
1456
1523
  const eventId = `${Date.now()}-${crypto.randomUUID()}`;
1457
1524
  const tmpPath = node_path_1.default.join(eventDir, `${eventId}.tmp`);
@@ -1459,6 +1526,13 @@ function enqueueWorkerContextEvent(event) {
1459
1526
  (0, node_fs_1.writeFileSync)(tmpPath, JSON.stringify(event), 'utf8');
1460
1527
  (0, node_fs_1.renameSync)(tmpPath, finalPath);
1461
1528
  }
1529
+ function enqueueWorkerContextEvent(event) {
1530
+ const eventDir = resolveWorkerEventQueueDir();
1531
+ if (!eventDir) {
1532
+ throw new Error('task_context_event_queue_missing');
1533
+ }
1534
+ enqueueWorkerEvent(eventDir, event);
1535
+ }
1462
1536
  function resolveWorkerTaskStateForWrite() {
1463
1537
  const state = readWorkerTaskState();
1464
1538
  if (!state) {
@@ -1838,6 +1912,9 @@ function buildCataloguePreviewPayload(cataloguePath, workspaceDir = node_path_1.
1838
1912
  const displayName = normalizePreviewString(app.displayName);
1839
1913
  if (displayName)
1840
1914
  catalogue.displayName = displayName;
1915
+ const subtitle = normalizePreviewString(app.subtitle);
1916
+ if (subtitle)
1917
+ catalogue.subtitle = subtitle;
1841
1918
  const description = normalizePreviewString(app.description);
1842
1919
  if (description)
1843
1920
  catalogue.description = description;
@@ -1850,6 +1927,9 @@ function buildCataloguePreviewPayload(cataloguePath, workspaceDir = node_path_1.
1850
1927
  const version = normalizePreviewString(app.version);
1851
1928
  if (version)
1852
1929
  catalogue.version = version;
1930
+ const primarySurface = normalizePreviewString(app.primarySurface);
1931
+ if (primarySurface)
1932
+ catalogue.primarySurface = primarySurface;
1853
1933
  const releaseNotes = normalizePreviewString(app.releaseNotes);
1854
1934
  if (releaseNotes)
1855
1935
  catalogue.releaseNotes = releaseNotes;
@@ -2486,18 +2566,35 @@ function buildAgentFailureCode(agent, result, request) {
2486
2566
  if (combinedOutput.includes('agent_task_creation_recovery_timeout')) {
2487
2567
  return `agent_task_creation_recovery_timeout:${provider}`;
2488
2568
  }
2569
+ const providerStatus = extractProviderStatusCode(combinedOutput);
2570
+ if (/\b(?:usage|spending|token|credit)?\s*(?:limit|quota)\s+(?:has been |was )?(?:reached|exceeded|exhausted)\b/i.test(combinedOutput)
2571
+ || /\byou(?:'ve| have) hit your (?:usage )?limit\b/i.test(combinedOutput)
2572
+ || /\binsufficient_quota\b/i.test(combinedOutput)) {
2573
+ return `agent_provider_quota_exhausted:${provider}`;
2574
+ }
2575
+ if (/\b(?:not logged in|authentication required|please (?:run|use) (?:the )?(?:login|\/login)|oauth token expired)\b/i.test(combinedOutput)
2576
+ || /\b(?:401|403)\s+(?:unauthorized|forbidden)\b/i.test(combinedOutput)) {
2577
+ return `agent_provider_not_authenticated:${provider}`;
2578
+ }
2489
2579
  if (request
2490
2580
  && (/\b(?:unknown|invalid|unsupported)\s+(?:model|effort)\b/i.test(combinedOutput)
2491
2581
  || /\b(?:model|effort)\b.{0,80}\b(?:not found|not available|does not exist|is unavailable|is not supported)\b/i.test(combinedOutput))) {
2492
2582
  return `requested_model_unavailable:${agent}:${request.model}:${request.reasoningEffort ?? ''}`;
2493
2583
  }
2494
- const providerStatus = extractProviderStatusCode(combinedOutput);
2495
2584
  if (providerStatus === 529 || (/\b529\b/.test(combinedOutput) && /\boverloaded?\b/i.test(combinedOutput))) {
2496
2585
  return `agent_provider_overloaded:${provider}:529`;
2497
2586
  }
2498
2587
  if (/\boverloaded?\b/i.test(combinedOutput)) {
2499
2588
  return `agent_provider_overloaded:${provider}`;
2500
2589
  }
2590
+ if (providerStatus === 429
2591
+ || providerStatus === 502
2592
+ || providerStatus === 503
2593
+ || providerStatus === 504
2594
+ || /\b(?:service|provider|api)\s+(?:is\s+)?(?:temporarily\s+)?unavailable\b/i.test(combinedOutput)
2595
+ || /\btoo many requests\b/i.test(combinedOutput)) {
2596
+ return `agent_provider_temporarily_unavailable:${provider}${providerStatus ? `:${providerStatus}` : ''}`;
2597
+ }
2501
2598
  if (result.timedOut) {
2502
2599
  return `agent_provider_timeout:${provider}`;
2503
2600
  }
@@ -2509,6 +2606,22 @@ function buildAgentFailureCode(agent, result, request) {
2509
2606
  }
2510
2607
  return `agent_unknown_exit:${provider}`;
2511
2608
  }
2609
+ function classifyAgentAvailabilityFailureReason(errorCode) {
2610
+ if (errorCode.startsWith('agent_provider_not_authenticated:')) {
2611
+ return 'agent_not_authenticated';
2612
+ }
2613
+ if (errorCode.startsWith('agent_provider_quota_exhausted:')) {
2614
+ return 'agent_quota_exhausted';
2615
+ }
2616
+ if (errorCode.startsWith('agent_provider_overloaded:')
2617
+ || errorCode.startsWith('agent_provider_temporarily_unavailable:')) {
2618
+ return 'agent_temporarily_unavailable';
2619
+ }
2620
+ if (errorCode.startsWith('requested_model_unavailable:')) {
2621
+ return 'agent_model_unsupported';
2622
+ }
2623
+ return null;
2624
+ }
2512
2625
  function describeAgentFailureForEvent(errorCode) {
2513
2626
  const [code, provider, detail] = errorCode.split(':');
2514
2627
  if (code === 'requested_model_unavailable') {
@@ -2517,8 +2630,19 @@ function describeAgentFailureForEvent(errorCode) {
2517
2630
  const providerLabel = provider === 'claude' ? 'Claude' : provider === 'codex' ? 'Codex' : 'The agent provider';
2518
2631
  if (code === 'agent_provider_overloaded') {
2519
2632
  return detail
2520
- ? `${providerLabel} is overloaded (API ${detail}). Retry this task in a few minutes.`
2521
- : `${providerLabel} is overloaded. Retry this task in a few minutes.`;
2633
+ ? `${providerLabel} is overloaded (API ${detail}). Reporting provider unavailability to the server.`
2634
+ : `${providerLabel} is overloaded. Reporting provider unavailability to the server.`;
2635
+ }
2636
+ if (code === 'agent_provider_quota_exhausted') {
2637
+ return `${providerLabel} has reached its usage limit. Reporting provider unavailability to the server.`;
2638
+ }
2639
+ if (code === 'agent_provider_not_authenticated') {
2640
+ return `${providerLabel} is not authenticated on this worker. Reporting provider unavailability to the server.`;
2641
+ }
2642
+ if (code === 'agent_provider_temporarily_unavailable') {
2643
+ return detail
2644
+ ? `${providerLabel} is temporarily unavailable (API ${detail}). Reporting provider unavailability to the server.`
2645
+ : `${providerLabel} is temporarily unavailable. Reporting provider unavailability to the server.`;
2522
2646
  }
2523
2647
  if (code === 'agent_provider_timeout') {
2524
2648
  return `${providerLabel} timed out before completing the task. Retry this task when the provider is healthy.`;
@@ -4496,6 +4620,27 @@ async function startWorker(options = {}) {
4496
4620
  if (shuttingDown) {
4497
4621
  throw new Error('worker_shutdown');
4498
4622
  }
4623
+ let initialCreatorProgressAttempted = false;
4624
+ const handleAgentChild = (controls) => {
4625
+ activeTerminators.set(task.id, controls.terminate);
4626
+ if (initialCreatorProgressAttempted) {
4627
+ return;
4628
+ }
4629
+ initialCreatorProgressAttempted = true;
4630
+ const initialProgress = resolveInitialCreatorProgress(task.kind, startsInCreationRecovery);
4631
+ if (!initialProgress) {
4632
+ return;
4633
+ }
4634
+ try {
4635
+ enqueueWorkerEvent(eventDir, initialProgress);
4636
+ queueEventDrain().catch((error) => {
4637
+ handleEventDrainFailure(error);
4638
+ });
4639
+ }
4640
+ catch (error) {
4641
+ console.error(`Warning: initial creator progress was not queued: ${error instanceof Error ? error.message : String(error)}`);
4642
+ }
4643
+ };
4499
4644
  const runAssignedAgent = async (input) => {
4500
4645
  if (assignment.agent.runtime === 'CODEX') {
4501
4646
  return runCodex({
@@ -4518,9 +4663,7 @@ async function startWorker(options = {}) {
4518
4663
  onTranscriptChunks: async (chunks) => {
4519
4664
  await appendTranscriptChunks(chunks);
4520
4665
  },
4521
- onChild: (controls) => {
4522
- activeTerminators.set(task.id, controls.terminate);
4523
- },
4666
+ onChild: handleAgentChild,
4524
4667
  cleanRoom,
4525
4668
  timeoutMs: input.timeoutMs,
4526
4669
  resumeSessionId: input.resumeSessionId,
@@ -4548,9 +4691,7 @@ async function startWorker(options = {}) {
4548
4691
  onTranscriptChunks: async (chunks) => {
4549
4692
  await appendTranscriptChunks(chunks);
4550
4693
  },
4551
- onChild: (controls) => {
4552
- activeTerminators.set(task.id, controls.terminate);
4553
- },
4694
+ onChild: handleAgentChild,
4554
4695
  cleanRoom,
4555
4696
  timeoutMs: input.timeoutMs,
4556
4697
  resumeSessionId: input.resumeSessionId,
@@ -4560,7 +4701,6 @@ async function startWorker(options = {}) {
4560
4701
  await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4561
4702
  workerKey,
4562
4703
  leaseToken,
4563
- attemptIndex: 0,
4564
4704
  reason: 'unavailable_runtime',
4565
4705
  message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4566
4706
  });
@@ -4686,6 +4826,7 @@ async function startWorker(options = {}) {
4686
4826
  model: assignment.agent.model,
4687
4827
  reasoningEffort,
4688
4828
  });
4829
+ const availabilityReason = classifyAgentAvailabilityFailureReason(failureCode);
4689
4830
  await client.workerCreateAgentTaskEvent(task.id, {
4690
4831
  workerKey,
4691
4832
  leaseToken,
@@ -4695,14 +4836,32 @@ async function startWorker(options = {}) {
4695
4836
  pct: null,
4696
4837
  payload: { error: failureCode },
4697
4838
  });
4698
- await failTaskWithRetry(client, task.id, {
4699
- workerKey,
4700
- leaseToken,
4701
- error: failureCode,
4702
- terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(agentFailureRawError(completedAgentResult), 'agent'),
4703
- result: agentRunResult,
4704
- });
4705
- await reportTelemetry('FAILED');
4839
+ if (availabilityReason) {
4840
+ retainWorkspace = false;
4841
+ await reportTelemetry('FAILED').catch((error) => {
4842
+ console.error(`Agent task ${task.id} telemetry failed before provider handoff: ${error instanceof Error ? error.message : String(error)}`);
4843
+ });
4844
+ const unavailable = await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4845
+ workerKey,
4846
+ leaseToken,
4847
+ reason: availabilityReason,
4848
+ message: failureCode,
4849
+ });
4850
+ fenced = true;
4851
+ console.error(unavailable.requeued
4852
+ ? `Agent task ${task.id} reported ${availabilityReason}; the server queued the next approved attempt.`
4853
+ : `Agent task ${task.id} reported ${availabilityReason}; no approved attempts remain.`);
4854
+ }
4855
+ else {
4856
+ await failTaskWithRetry(client, task.id, {
4857
+ workerKey,
4858
+ leaseToken,
4859
+ error: failureCode,
4860
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(failureCode, 'agent'),
4861
+ result: agentRunResult,
4862
+ });
4863
+ await reportTelemetry('FAILED');
4864
+ }
4706
4865
  }
4707
4866
  }
4708
4867
  else {
@@ -5114,7 +5273,7 @@ async function reportTask(options) {
5114
5273
  const done = options.done?.trim() || null;
5115
5274
  const current = options.current?.trim() || null;
5116
5275
  if (!message && !done && !current) {
5117
- (0, messages_1.printErrorWithHelp)('Task report requires a message, done, or current value.', ['Example: playdrop task report --phase build --done "Built the first loop" --current "Tuning controls"'], { command: 'task report' });
5276
+ (0, messages_1.printErrorWithHelp)('Task report requires a message, done, or current value.', ['Example: playdrop task report --phase greybox-core-loop --done "Built the first loop" --current "Tuning controls"'], { command: 'task report' });
5118
5277
  node_process_1.default.exitCode = 1;
5119
5278
  return;
5120
5279
  }
@@ -5150,6 +5309,128 @@ function showTaskContext() {
5150
5309
  },
5151
5310
  }, null, 2));
5152
5311
  }
5312
+ function readWorkerTaskMaterialReceipts(workspaceDir) {
5313
+ const receiptPath = workerTaskMaterialsPath(workspaceDir);
5314
+ if (!(0, node_fs_1.existsSync)(receiptPath)) {
5315
+ return [];
5316
+ }
5317
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(receiptPath, 'utf8'));
5318
+ if (!Array.isArray(parsed)) {
5319
+ throw new Error('task_material_receipts_invalid');
5320
+ }
5321
+ return parsed.map((entry) => {
5322
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
5323
+ throw new Error('task_material_receipts_invalid');
5324
+ }
5325
+ const candidate = entry;
5326
+ const filePath = typeof candidate.filePath === 'string' ? candidate.filePath.trim() : '';
5327
+ const sha256 = typeof candidate.sha256 === 'string' ? candidate.sha256.trim() : '';
5328
+ const presentation = candidate.presentation;
5329
+ const title = typeof candidate.title === 'string' ? candidate.title.trim() : '';
5330
+ if (!node_path_1.default.isAbsolute(filePath)
5331
+ || !/^[a-f0-9]{64}$/.test(sha256)
5332
+ || (presentation !== 'frame' && presentation !== 'asset')
5333
+ || !title) {
5334
+ throw new Error('task_material_receipts_invalid');
5335
+ }
5336
+ return { filePath, sha256, presentation, title };
5337
+ });
5338
+ }
5339
+ function recordWorkerTaskMaterialReceipt(workspaceDir, receipt) {
5340
+ const receipts = readWorkerTaskMaterialReceipts(workspaceDir);
5341
+ const retained = receipts.filter((entry) => (entry.filePath !== receipt.filePath
5342
+ || entry.presentation !== receipt.presentation));
5343
+ retained.push(receipt);
5344
+ (0, node_fs_1.writeFileSync)(workerTaskMaterialsPath(workspaceDir), `${JSON.stringify(retained, null, 2)}\n`, 'utf8');
5345
+ }
5346
+ async function assertTaskAssetMaterialIsTransparentPng(filePath) {
5347
+ let metadata;
5348
+ let stats;
5349
+ try {
5350
+ metadata = await (0, sharp_1.default)(filePath, { failOn: 'error' }).metadata();
5351
+ stats = await (0, sharp_1.default)(filePath, { failOn: 'error' }).stats();
5352
+ }
5353
+ catch {
5354
+ throw new Error(`task_material_asset_decode_failed:${filePath}`);
5355
+ }
5356
+ if (metadata.format !== 'png') {
5357
+ throw new Error(`task_material_asset_must_be_png:${filePath}`);
5358
+ }
5359
+ if (metadata.hasAlpha !== true || stats.isOpaque) {
5360
+ throw new Error(`task_material_asset_background_not_removed:${filePath}`);
5361
+ }
5362
+ }
5363
+ async function materialTask(options) {
5364
+ try {
5365
+ const fileOption = options.file?.trim() ?? '';
5366
+ const title = options.title?.trim() ?? '';
5367
+ if (!fileOption || !title || title.length > 60 || title.split(/\s+/).length > 3) {
5368
+ throw new Error('task_material_file_and_title_required');
5369
+ }
5370
+ const filePath = node_path_1.default.resolve(fileOption);
5371
+ if (!(0, node_fs_1.existsSync)(filePath) || !(0, node_fs_1.statSync)(filePath).isFile()) {
5372
+ throw new Error(`task_material_file_not_found:${fileOption}`);
5373
+ }
5374
+ const posterOption = options.poster?.trim() ?? '';
5375
+ const posterPath = posterOption ? node_path_1.default.resolve(posterOption) : null;
5376
+ if (posterPath && (!(0, node_fs_1.existsSync)(posterPath) || !(0, node_fs_1.statSync)(posterPath).isFile())) {
5377
+ throw new Error(`task_material_poster_not_found:${posterOption}`);
5378
+ }
5379
+ const presentation = options.asset ? 'asset' : 'frame';
5380
+ if (presentation === 'asset') {
5381
+ if (posterPath) {
5382
+ throw new Error('task_material_asset_poster_not_allowed');
5383
+ }
5384
+ await assertTaskAssetMaterialIsTransparentPng(filePath);
5385
+ }
5386
+ const taskContext = readTaskContextFile();
5387
+ const ctx = await resolveTaskCommandContext('task material', options.env, taskContext);
5388
+ if (!ctx) {
5389
+ throw new Error('task_material_context_unavailable');
5390
+ }
5391
+ const fileBuffer = (0, node_fs_1.readFileSync)(filePath);
5392
+ const posterBuffer = posterPath ? (0, node_fs_1.readFileSync)(posterPath) : null;
5393
+ const clientKey = (0, node_crypto_1.createHash)('sha256')
5394
+ .update([
5395
+ String(taskContext.taskId),
5396
+ String(taskContext.attempt),
5397
+ node_path_1.default.relative(node_process_1.default.cwd(), filePath),
5398
+ (0, node_crypto_1.createHash)('sha256').update(fileBuffer).digest('hex'),
5399
+ title,
5400
+ posterBuffer ? (0, node_crypto_1.createHash)('sha256').update(posterBuffer).digest('hex') : '',
5401
+ presentation,
5402
+ ].join(':'))
5403
+ .digest('hex')
5404
+ .slice(0, 40);
5405
+ await ctx.client.workerUploadAgentTaskMaterial(taskContext.taskId, {
5406
+ file: new Blob([new Uint8Array(fileBuffer)]),
5407
+ fileName: node_path_1.default.basename(filePath),
5408
+ title,
5409
+ clientKey,
5410
+ presentation,
5411
+ phase: options.phase?.trim() || undefined,
5412
+ taskToken: taskContext.taskToken,
5413
+ ...(posterBuffer && posterPath
5414
+ ? {
5415
+ poster: new Blob([new Uint8Array(posterBuffer)]),
5416
+ posterFileName: node_path_1.default.basename(posterPath),
5417
+ }
5418
+ : {}),
5419
+ });
5420
+ recordWorkerTaskMaterialReceipt(resolveTaskWorkspaceDir(), {
5421
+ filePath,
5422
+ sha256: (0, node_crypto_1.createHash)('sha256').update(fileBuffer).digest('hex'),
5423
+ presentation,
5424
+ title,
5425
+ });
5426
+ (0, output_1.printSuccess)(`Task material shared: ${title}`);
5427
+ }
5428
+ catch (error) {
5429
+ const message = error instanceof Error ? error.message : String(error);
5430
+ console.warn(`Warning: task material was not shared: ${message}`);
5431
+ node_process_1.default.exitCode = 0;
5432
+ }
5433
+ }
5153
5434
  async function reportCatalogueTask(options) {
5154
5435
  const message = options.message?.trim();
5155
5436
  if (!message) {
@@ -5216,6 +5497,38 @@ async function uploadTask(options = {}) {
5216
5497
  }
5217
5498
  const uploadKind = taskContext.kind;
5218
5499
  const workspaceDir = resolveTaskWorkspaceDir();
5500
+ if (options.preflightOnly) {
5501
+ const ctx = await resolveTaskCommandContext('task upload --preflight-only', options.env, taskContext);
5502
+ if (!ctx) {
5503
+ return;
5504
+ }
5505
+ const projectDir = discoverWorkerProjectRoot(node_process_1.default.cwd());
5506
+ const result = await (0, upload_1.preflightWorkerAppProject)({
5507
+ client: ctx.client,
5508
+ taskId: taskContext.taskId,
5509
+ taskToken: taskContext.taskToken,
5510
+ kind: uploadKind,
5511
+ executionTarget: taskContext.target,
5512
+ expectedAppName: taskContext.outputAppName ?? undefined,
5513
+ expectedDisplayName: taskContext.outputDisplayName,
5514
+ expectedSubtitle: taskContext.outputSubtitle,
5515
+ expectedPrimarySurface: taskContext.outputPrimarySurface,
5516
+ remixSourceRef: taskContext.remixSourceRef ?? null,
5517
+ playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5518
+ creatorRequest: taskContext.creatorRequest,
5519
+ projectDir,
5520
+ creatorUsername: taskContext.creatorUsername,
5521
+ apiBase: ctx.envConfig.apiBase,
5522
+ webBase: ctx.envConfig.webBase ?? null,
5523
+ token: ctx.token,
5524
+ user: ctx.user,
5525
+ });
5526
+ for (const warning of result.warnings) {
5527
+ console.error(`Preflight warning: ${warning}`);
5528
+ }
5529
+ (0, output_1.printSuccess)(`Task upload preflight passed for ${result.creatorUsername}/${result.appName}. Final capture can start.`);
5530
+ return;
5531
+ }
5219
5532
  if ((0, node_fs_1.existsSync)(workerTaskUploadResultPath(workspaceDir))) {
5220
5533
  const existingResult = readTaskUploadResultFile(workspaceDir);
5221
5534
  assertTaskUploadResultMatchesContext({
@@ -5238,6 +5551,9 @@ async function uploadTask(options = {}) {
5238
5551
  kind: uploadKind,
5239
5552
  executionTarget: taskContext.target,
5240
5553
  expectedAppName: taskContext.outputAppName ?? undefined,
5554
+ expectedDisplayName: taskContext.outputDisplayName,
5555
+ expectedSubtitle: taskContext.outputSubtitle,
5556
+ expectedPrimarySurface: taskContext.outputPrimarySurface,
5241
5557
  remixSourceRef: taskContext.remixSourceRef ?? null,
5242
5558
  playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5243
5559
  creatorRequest: taskContext.creatorRequest,
@@ -5271,26 +5587,55 @@ async function claimSlugTask(options) {
5271
5587
  }
5272
5588
  const appName = normalizeTaskClaimSlugAppName(options.appName);
5273
5589
  const displayName = normalizeTaskClaimSlugDisplayName(options.displayName);
5590
+ const subtitle = normalizeTaskClaimSlugSubtitle(options.subtitle);
5591
+ const primarySurface = normalizeTaskClaimSlugPrimarySurface(options.primarySurface);
5274
5592
  const ctx = await resolveTaskCommandContext('task claim-slug', options.env, taskContext);
5275
5593
  if (!ctx) {
5276
5594
  return;
5277
5595
  }
5278
- const response = await ctx.client.workerClaimAgentTaskSlug(taskContext.taskId, {
5596
+ const request = {
5279
5597
  appName,
5280
5598
  displayName,
5281
- });
5599
+ ...(subtitle ? { subtitle } : {}),
5600
+ ...(primarySurface ? { primarySurface } : {}),
5601
+ };
5602
+ const response = await ctx.client.workerClaimAgentTaskSlug(taskContext.taskId, request);
5282
5603
  const claimedAppName = typeof response.appName === 'string' && response.appName.trim()
5283
5604
  ? response.appName.trim()
5284
5605
  : '';
5285
5606
  if (claimedAppName !== appName) {
5286
5607
  throw new Error('task_claim_slug_response_mismatch');
5287
5608
  }
5609
+ const hasResponseDisplayName = Object.prototype.hasOwnProperty.call(response, 'displayName');
5610
+ const responseDisplayName = typeof response.displayName === 'string' && response.displayName.trim()
5611
+ ? response.displayName.trim()
5612
+ : '';
5613
+ if (hasResponseDisplayName && responseDisplayName !== displayName) {
5614
+ throw new Error('task_claim_identity_response_mismatch');
5615
+ }
5616
+ const hasResponseSubtitle = Object.prototype.hasOwnProperty.call(response, 'subtitle');
5617
+ const responseSubtitle = typeof response.subtitle === 'string' && response.subtitle.trim()
5618
+ ? response.subtitle.trim()
5619
+ : null;
5620
+ const hasResponsePrimarySurface = Object.prototype.hasOwnProperty.call(response, 'primarySurface');
5621
+ const responsePrimarySurface = normalizeWorkerInstrumentSurface(response.primarySurface);
5622
+ if ((subtitle && hasResponseSubtitle && responseSubtitle !== subtitle)
5623
+ || (primarySurface && hasResponsePrimarySurface && responsePrimarySurface !== primarySurface)) {
5624
+ throw new Error('task_claim_identity_response_mismatch');
5625
+ }
5626
+ const claimedSubtitle = subtitle ?? responseSubtitle ?? taskContext.outputSubtitle;
5627
+ const claimedPrimarySurface = primarySurface ?? responsePrimarySurface ?? taskContext.outputPrimarySurface;
5288
5628
  const workspaceDir = resolveTaskWorkspaceDir();
5289
5629
  await writeWorkerTaskContextFile(workspaceDir, {
5290
5630
  ...taskContext,
5291
5631
  outputAppName: claimedAppName,
5632
+ outputDisplayName: displayName,
5633
+ outputSubtitle: claimedSubtitle,
5634
+ outputPrimarySurface: claimedPrimarySurface,
5292
5635
  });
5293
- (0, output_1.printSuccess)(`Claimed app slug "${claimedAppName}" for ${displayName}.`);
5636
+ (0, output_1.printSuccess)(claimedPrimarySurface
5637
+ ? `Claimed "${displayName}" (${claimedAppName}) for ${claimedPrimarySurface}.`
5638
+ : `Claimed app slug "${claimedAppName}" for ${displayName}.`);
5294
5639
  }
5295
5640
  async function completeTask(options) {
5296
5641
  const taskContext = readTaskContextFile();
@@ -5298,9 +5643,12 @@ async function completeTask(options) {
5298
5643
  throw new Error('task_done_not_allowed_for_game_review');
5299
5644
  }
5300
5645
  const summary = typeof options.summary === 'string' ? options.summary.trim() : '';
5301
- if (!summary || summary.length > 200 || /[\r\n]/.test(summary)) {
5646
+ if (!summary || /[\r\n]/.test(summary)) {
5302
5647
  throw new Error('task_done_summary_required');
5303
5648
  }
5649
+ if (summary.length > 200) {
5650
+ throw new Error('task_done_summary_too_long:max_200_characters');
5651
+ }
5304
5652
  const ctx = await resolveTaskCommandContext('task done', options.env, taskContext);
5305
5653
  if (!ctx) {
5306
5654
  return;