@playdrop/playdrop-cli 0.13.16 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +52 -0
  5. package/dist/commands/captureListing.js +213 -12
  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 +381 -29
  12. package/dist/index.js +17 -2
  13. package/dist/listingPreflight.d.ts +2 -1
  14. package/dist/listingPreflight.js +63 -35
  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,36 @@ 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
- if (/\boverloaded?\b/i.test(combinedOutput)) {
2587
+ if (/\boverloaded?\b/i.test(combinedOutput)
2588
+ || /\b(?:selected\s+)?model\s+is\s+at\s+capacity\b/i.test(combinedOutput)) {
2499
2589
  return `agent_provider_overloaded:${provider}`;
2500
2590
  }
2591
+ if (providerStatus === 429
2592
+ || providerStatus === 502
2593
+ || providerStatus === 503
2594
+ || providerStatus === 504
2595
+ || /\b(?:service|provider|api)\s+(?:is\s+)?(?:temporarily\s+)?unavailable\b/i.test(combinedOutput)
2596
+ || /\btoo many requests\b/i.test(combinedOutput)) {
2597
+ return `agent_provider_temporarily_unavailable:${provider}${providerStatus ? `:${providerStatus}` : ''}`;
2598
+ }
2501
2599
  if (result.timedOut) {
2502
2600
  return `agent_provider_timeout:${provider}`;
2503
2601
  }
@@ -2509,6 +2607,22 @@ function buildAgentFailureCode(agent, result, request) {
2509
2607
  }
2510
2608
  return `agent_unknown_exit:${provider}`;
2511
2609
  }
2610
+ function classifyAgentAvailabilityFailureReason(errorCode) {
2611
+ if (errorCode.startsWith('agent_provider_not_authenticated:')) {
2612
+ return 'agent_not_authenticated';
2613
+ }
2614
+ if (errorCode.startsWith('agent_provider_quota_exhausted:')) {
2615
+ return 'agent_quota_exhausted';
2616
+ }
2617
+ if (errorCode.startsWith('agent_provider_overloaded:')
2618
+ || errorCode.startsWith('agent_provider_temporarily_unavailable:')) {
2619
+ return 'agent_temporarily_unavailable';
2620
+ }
2621
+ if (errorCode.startsWith('requested_model_unavailable:')) {
2622
+ return 'agent_model_unsupported';
2623
+ }
2624
+ return null;
2625
+ }
2512
2626
  function describeAgentFailureForEvent(errorCode) {
2513
2627
  const [code, provider, detail] = errorCode.split(':');
2514
2628
  if (code === 'requested_model_unavailable') {
@@ -2517,8 +2631,19 @@ function describeAgentFailureForEvent(errorCode) {
2517
2631
  const providerLabel = provider === 'claude' ? 'Claude' : provider === 'codex' ? 'Codex' : 'The agent provider';
2518
2632
  if (code === 'agent_provider_overloaded') {
2519
2633
  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.`;
2634
+ ? `${providerLabel} is overloaded (API ${detail}). Reporting provider unavailability to the server.`
2635
+ : `${providerLabel} is overloaded. Reporting provider unavailability to the server.`;
2636
+ }
2637
+ if (code === 'agent_provider_quota_exhausted') {
2638
+ return `${providerLabel} has reached its usage limit. Reporting provider unavailability to the server.`;
2639
+ }
2640
+ if (code === 'agent_provider_not_authenticated') {
2641
+ return `${providerLabel} is not authenticated on this worker. Reporting provider unavailability to the server.`;
2642
+ }
2643
+ if (code === 'agent_provider_temporarily_unavailable') {
2644
+ return detail
2645
+ ? `${providerLabel} is temporarily unavailable (API ${detail}). Reporting provider unavailability to the server.`
2646
+ : `${providerLabel} is temporarily unavailable. Reporting provider unavailability to the server.`;
2522
2647
  }
2523
2648
  if (code === 'agent_provider_timeout') {
2524
2649
  return `${providerLabel} timed out before completing the task. Retry this task when the provider is healthy.`;
@@ -4496,6 +4621,27 @@ async function startWorker(options = {}) {
4496
4621
  if (shuttingDown) {
4497
4622
  throw new Error('worker_shutdown');
4498
4623
  }
4624
+ let initialCreatorProgressAttempted = false;
4625
+ const handleAgentChild = (controls) => {
4626
+ activeTerminators.set(task.id, controls.terminate);
4627
+ if (initialCreatorProgressAttempted) {
4628
+ return;
4629
+ }
4630
+ initialCreatorProgressAttempted = true;
4631
+ const initialProgress = resolveInitialCreatorProgress(task.kind, startsInCreationRecovery);
4632
+ if (!initialProgress) {
4633
+ return;
4634
+ }
4635
+ try {
4636
+ enqueueWorkerEvent(eventDir, initialProgress);
4637
+ queueEventDrain().catch((error) => {
4638
+ handleEventDrainFailure(error);
4639
+ });
4640
+ }
4641
+ catch (error) {
4642
+ console.error(`Warning: initial creator progress was not queued: ${error instanceof Error ? error.message : String(error)}`);
4643
+ }
4644
+ };
4499
4645
  const runAssignedAgent = async (input) => {
4500
4646
  if (assignment.agent.runtime === 'CODEX') {
4501
4647
  return runCodex({
@@ -4518,9 +4664,7 @@ async function startWorker(options = {}) {
4518
4664
  onTranscriptChunks: async (chunks) => {
4519
4665
  await appendTranscriptChunks(chunks);
4520
4666
  },
4521
- onChild: (controls) => {
4522
- activeTerminators.set(task.id, controls.terminate);
4523
- },
4667
+ onChild: handleAgentChild,
4524
4668
  cleanRoom,
4525
4669
  timeoutMs: input.timeoutMs,
4526
4670
  resumeSessionId: input.resumeSessionId,
@@ -4548,9 +4692,7 @@ async function startWorker(options = {}) {
4548
4692
  onTranscriptChunks: async (chunks) => {
4549
4693
  await appendTranscriptChunks(chunks);
4550
4694
  },
4551
- onChild: (controls) => {
4552
- activeTerminators.set(task.id, controls.terminate);
4553
- },
4695
+ onChild: handleAgentChild,
4554
4696
  cleanRoom,
4555
4697
  timeoutMs: input.timeoutMs,
4556
4698
  resumeSessionId: input.resumeSessionId,
@@ -4560,7 +4702,6 @@ async function startWorker(options = {}) {
4560
4702
  await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4561
4703
  workerKey,
4562
4704
  leaseToken,
4563
- attemptIndex: 0,
4564
4705
  reason: 'unavailable_runtime',
4565
4706
  message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
4566
4707
  });
@@ -4686,6 +4827,7 @@ async function startWorker(options = {}) {
4686
4827
  model: assignment.agent.model,
4687
4828
  reasoningEffort,
4688
4829
  });
4830
+ const availabilityReason = classifyAgentAvailabilityFailureReason(failureCode);
4689
4831
  await client.workerCreateAgentTaskEvent(task.id, {
4690
4832
  workerKey,
4691
4833
  leaseToken,
@@ -4695,14 +4837,32 @@ async function startWorker(options = {}) {
4695
4837
  pct: null,
4696
4838
  payload: { error: failureCode },
4697
4839
  });
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');
4840
+ if (availabilityReason) {
4841
+ retainWorkspace = false;
4842
+ await reportTelemetry('FAILED').catch((error) => {
4843
+ console.error(`Agent task ${task.id} telemetry failed before provider handoff: ${error instanceof Error ? error.message : String(error)}`);
4844
+ });
4845
+ const unavailable = await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4846
+ workerKey,
4847
+ leaseToken,
4848
+ reason: availabilityReason,
4849
+ message: failureCode,
4850
+ });
4851
+ fenced = true;
4852
+ console.error(unavailable.requeued
4853
+ ? `Agent task ${task.id} reported ${availabilityReason}; the server queued the next approved attempt.`
4854
+ : `Agent task ${task.id} reported ${availabilityReason}; no approved attempts remain.`);
4855
+ }
4856
+ else {
4857
+ await failTaskWithRetry(client, task.id, {
4858
+ workerKey,
4859
+ leaseToken,
4860
+ error: failureCode,
4861
+ terminalReason: (0, types_1.classifyAgentTaskTerminalReason)(failureCode, 'agent'),
4862
+ result: agentRunResult,
4863
+ });
4864
+ await reportTelemetry('FAILED');
4865
+ }
4706
4866
  }
4707
4867
  }
4708
4868
  else {
@@ -5114,7 +5274,7 @@ async function reportTask(options) {
5114
5274
  const done = options.done?.trim() || null;
5115
5275
  const current = options.current?.trim() || null;
5116
5276
  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' });
5277
+ (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
5278
  node_process_1.default.exitCode = 1;
5119
5279
  return;
5120
5280
  }
@@ -5150,6 +5310,131 @@ function showTaskContext() {
5150
5310
  },
5151
5311
  }, null, 2));
5152
5312
  }
5313
+ function readWorkerTaskMaterialReceipts(workspaceDir) {
5314
+ const receiptPath = workerTaskMaterialsPath(workspaceDir);
5315
+ if (!(0, node_fs_1.existsSync)(receiptPath)) {
5316
+ return [];
5317
+ }
5318
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(receiptPath, 'utf8'));
5319
+ if (!Array.isArray(parsed)) {
5320
+ throw new Error('task_material_receipts_invalid');
5321
+ }
5322
+ return parsed.map((entry) => {
5323
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
5324
+ throw new Error('task_material_receipts_invalid');
5325
+ }
5326
+ const candidate = entry;
5327
+ const filePath = typeof candidate.filePath === 'string' ? candidate.filePath.trim() : '';
5328
+ const sha256 = typeof candidate.sha256 === 'string' ? candidate.sha256.trim() : '';
5329
+ const presentation = candidate.presentation;
5330
+ const title = typeof candidate.title === 'string' ? candidate.title.trim() : '';
5331
+ if (!node_path_1.default.isAbsolute(filePath)
5332
+ || !/^[a-f0-9]{64}$/.test(sha256)
5333
+ || (presentation !== 'frame' && presentation !== 'asset')
5334
+ || !title) {
5335
+ throw new Error('task_material_receipts_invalid');
5336
+ }
5337
+ return { filePath, sha256, presentation, title };
5338
+ });
5339
+ }
5340
+ function recordWorkerTaskMaterialReceipt(workspaceDir, receipt) {
5341
+ const receipts = readWorkerTaskMaterialReceipts(workspaceDir);
5342
+ const retained = receipts.filter((entry) => (entry.filePath !== receipt.filePath
5343
+ || entry.presentation !== receipt.presentation));
5344
+ retained.push(receipt);
5345
+ (0, node_fs_1.writeFileSync)(workerTaskMaterialsPath(workspaceDir), `${JSON.stringify(retained, null, 2)}\n`, 'utf8');
5346
+ }
5347
+ async function assertTaskAssetMaterialIsTransparentPng(filePath) {
5348
+ let metadata;
5349
+ let stats;
5350
+ try {
5351
+ metadata = await (0, sharp_1.default)(filePath, { failOn: 'error' }).metadata();
5352
+ stats = await (0, sharp_1.default)(filePath, { failOn: 'error' }).stats();
5353
+ }
5354
+ catch {
5355
+ throw new Error(`task_material_asset_decode_failed:${filePath}`);
5356
+ }
5357
+ if (metadata.format !== 'png') {
5358
+ throw new Error(`task_material_asset_must_be_png:${filePath}`);
5359
+ }
5360
+ if (metadata.hasAlpha !== true || stats.isOpaque) {
5361
+ throw new Error(`task_material_asset_background_not_removed:${filePath}`);
5362
+ }
5363
+ }
5364
+ async function materialTask(options) {
5365
+ try {
5366
+ const fileOption = options.file?.trim() ?? '';
5367
+ const title = options.title?.trim() ?? '';
5368
+ if (!fileOption || !title || title.length > 60 || title.split(/\s+/).length > 3) {
5369
+ throw new Error('task_material_file_and_title_required');
5370
+ }
5371
+ const filePath = node_path_1.default.resolve(fileOption);
5372
+ if (!(0, node_fs_1.existsSync)(filePath) || !(0, node_fs_1.statSync)(filePath).isFile()) {
5373
+ throw new Error(`task_material_file_not_found:${fileOption}`);
5374
+ }
5375
+ const posterOption = options.poster?.trim() ?? '';
5376
+ const posterPath = posterOption ? node_path_1.default.resolve(posterOption) : null;
5377
+ if (posterPath && (!(0, node_fs_1.existsSync)(posterPath) || !(0, node_fs_1.statSync)(posterPath).isFile())) {
5378
+ throw new Error(`task_material_poster_not_found:${posterOption}`);
5379
+ }
5380
+ const presentation = options.asset ? 'asset' : 'frame';
5381
+ if (presentation === 'asset') {
5382
+ if (posterPath) {
5383
+ throw new Error('task_material_asset_poster_not_allowed');
5384
+ }
5385
+ await assertTaskAssetMaterialIsTransparentPng(filePath);
5386
+ }
5387
+ const taskContext = readTaskContextFile();
5388
+ const ctx = await resolveTaskCommandContext('task material', options.env, taskContext);
5389
+ if (!ctx) {
5390
+ throw new Error('task_material_context_unavailable');
5391
+ }
5392
+ const fileBuffer = (0, node_fs_1.readFileSync)(filePath);
5393
+ const posterBuffer = posterPath ? (0, node_fs_1.readFileSync)(posterPath) : null;
5394
+ const clientKey = (0, node_crypto_1.createHash)('sha256')
5395
+ .update([
5396
+ String(taskContext.taskId),
5397
+ String(taskContext.attempt),
5398
+ node_path_1.default.relative(node_process_1.default.cwd(), filePath),
5399
+ (0, node_crypto_1.createHash)('sha256').update(fileBuffer).digest('hex'),
5400
+ title,
5401
+ posterBuffer ? (0, node_crypto_1.createHash)('sha256').update(posterBuffer).digest('hex') : '',
5402
+ presentation,
5403
+ ].join(':'))
5404
+ .digest('hex')
5405
+ .slice(0, 40);
5406
+ await ctx.client.workerUploadAgentTaskMaterial(taskContext.taskId, {
5407
+ file: new Blob([new Uint8Array(fileBuffer)]),
5408
+ fileName: node_path_1.default.basename(filePath),
5409
+ title,
5410
+ clientKey,
5411
+ presentation,
5412
+ phase: options.phase?.trim() || undefined,
5413
+ taskToken: taskContext.taskToken,
5414
+ ...(posterBuffer && posterPath
5415
+ ? {
5416
+ poster: new Blob([new Uint8Array(posterBuffer)]),
5417
+ posterFileName: node_path_1.default.basename(posterPath),
5418
+ }
5419
+ : {}),
5420
+ });
5421
+ recordWorkerTaskMaterialReceipt(resolveTaskWorkspaceDir(), {
5422
+ filePath,
5423
+ sha256: (0, node_crypto_1.createHash)('sha256').update(fileBuffer).digest('hex'),
5424
+ presentation,
5425
+ title,
5426
+ });
5427
+ (0, output_1.printSuccess)(`Task material shared: ${title}`);
5428
+ }
5429
+ catch (error) {
5430
+ const message = error instanceof Error ? error.message : String(error);
5431
+ const suggestion = message.startsWith('task_material_asset_background_not_removed:')
5432
+ ? ' Remove the background and retry with --asset, or omit --asset if the image is honestly useful as a full frame.'
5433
+ : '';
5434
+ console.warn(`Warning: task material was not shared: ${message}.${suggestion}`);
5435
+ node_process_1.default.exitCode = 0;
5436
+ }
5437
+ }
5153
5438
  async function reportCatalogueTask(options) {
5154
5439
  const message = options.message?.trim();
5155
5440
  if (!message) {
@@ -5216,6 +5501,38 @@ async function uploadTask(options = {}) {
5216
5501
  }
5217
5502
  const uploadKind = taskContext.kind;
5218
5503
  const workspaceDir = resolveTaskWorkspaceDir();
5504
+ if (options.preflightOnly) {
5505
+ const ctx = await resolveTaskCommandContext('task upload --preflight-only', options.env, taskContext);
5506
+ if (!ctx) {
5507
+ return;
5508
+ }
5509
+ const projectDir = discoverWorkerProjectRoot(node_process_1.default.cwd());
5510
+ const result = await (0, upload_1.preflightWorkerAppProject)({
5511
+ client: ctx.client,
5512
+ taskId: taskContext.taskId,
5513
+ taskToken: taskContext.taskToken,
5514
+ kind: uploadKind,
5515
+ executionTarget: taskContext.target,
5516
+ expectedAppName: taskContext.outputAppName ?? undefined,
5517
+ expectedDisplayName: taskContext.outputDisplayName,
5518
+ expectedSubtitle: taskContext.outputSubtitle,
5519
+ expectedPrimarySurface: taskContext.outputPrimarySurface,
5520
+ remixSourceRef: taskContext.remixSourceRef ?? null,
5521
+ playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5522
+ creatorRequest: taskContext.creatorRequest,
5523
+ projectDir,
5524
+ creatorUsername: taskContext.creatorUsername,
5525
+ apiBase: ctx.envConfig.apiBase,
5526
+ webBase: ctx.envConfig.webBase ?? null,
5527
+ token: ctx.token,
5528
+ user: ctx.user,
5529
+ });
5530
+ for (const warning of result.warnings) {
5531
+ console.error(`Preflight warning: ${warning}`);
5532
+ }
5533
+ (0, output_1.printSuccess)(`Task upload preflight passed for ${result.creatorUsername}/${result.appName}. Final capture can start.`);
5534
+ return;
5535
+ }
5219
5536
  if ((0, node_fs_1.existsSync)(workerTaskUploadResultPath(workspaceDir))) {
5220
5537
  const existingResult = readTaskUploadResultFile(workspaceDir);
5221
5538
  assertTaskUploadResultMatchesContext({
@@ -5238,6 +5555,9 @@ async function uploadTask(options = {}) {
5238
5555
  kind: uploadKind,
5239
5556
  executionTarget: taskContext.target,
5240
5557
  expectedAppName: taskContext.outputAppName ?? undefined,
5558
+ expectedDisplayName: taskContext.outputDisplayName,
5559
+ expectedSubtitle: taskContext.outputSubtitle,
5560
+ expectedPrimarySurface: taskContext.outputPrimarySurface,
5241
5561
  remixSourceRef: taskContext.remixSourceRef ?? null,
5242
5562
  playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5243
5563
  creatorRequest: taskContext.creatorRequest,
@@ -5271,26 +5591,55 @@ async function claimSlugTask(options) {
5271
5591
  }
5272
5592
  const appName = normalizeTaskClaimSlugAppName(options.appName);
5273
5593
  const displayName = normalizeTaskClaimSlugDisplayName(options.displayName);
5594
+ const subtitle = normalizeTaskClaimSlugSubtitle(options.subtitle);
5595
+ const primarySurface = normalizeTaskClaimSlugPrimarySurface(options.primarySurface);
5274
5596
  const ctx = await resolveTaskCommandContext('task claim-slug', options.env, taskContext);
5275
5597
  if (!ctx) {
5276
5598
  return;
5277
5599
  }
5278
- const response = await ctx.client.workerClaimAgentTaskSlug(taskContext.taskId, {
5600
+ const request = {
5279
5601
  appName,
5280
5602
  displayName,
5281
- });
5603
+ ...(subtitle ? { subtitle } : {}),
5604
+ ...(primarySurface ? { primarySurface } : {}),
5605
+ };
5606
+ const response = await ctx.client.workerClaimAgentTaskSlug(taskContext.taskId, request);
5282
5607
  const claimedAppName = typeof response.appName === 'string' && response.appName.trim()
5283
5608
  ? response.appName.trim()
5284
5609
  : '';
5285
5610
  if (claimedAppName !== appName) {
5286
5611
  throw new Error('task_claim_slug_response_mismatch');
5287
5612
  }
5613
+ const hasResponseDisplayName = Object.prototype.hasOwnProperty.call(response, 'displayName');
5614
+ const responseDisplayName = typeof response.displayName === 'string' && response.displayName.trim()
5615
+ ? response.displayName.trim()
5616
+ : '';
5617
+ if (hasResponseDisplayName && responseDisplayName !== displayName) {
5618
+ throw new Error('task_claim_identity_response_mismatch');
5619
+ }
5620
+ const hasResponseSubtitle = Object.prototype.hasOwnProperty.call(response, 'subtitle');
5621
+ const responseSubtitle = typeof response.subtitle === 'string' && response.subtitle.trim()
5622
+ ? response.subtitle.trim()
5623
+ : null;
5624
+ const hasResponsePrimarySurface = Object.prototype.hasOwnProperty.call(response, 'primarySurface');
5625
+ const responsePrimarySurface = normalizeWorkerInstrumentSurface(response.primarySurface);
5626
+ if ((subtitle && hasResponseSubtitle && responseSubtitle !== subtitle)
5627
+ || (primarySurface && hasResponsePrimarySurface && responsePrimarySurface !== primarySurface)) {
5628
+ throw new Error('task_claim_identity_response_mismatch');
5629
+ }
5630
+ const claimedSubtitle = subtitle ?? responseSubtitle ?? taskContext.outputSubtitle;
5631
+ const claimedPrimarySurface = primarySurface ?? responsePrimarySurface ?? taskContext.outputPrimarySurface;
5288
5632
  const workspaceDir = resolveTaskWorkspaceDir();
5289
5633
  await writeWorkerTaskContextFile(workspaceDir, {
5290
5634
  ...taskContext,
5291
5635
  outputAppName: claimedAppName,
5636
+ outputDisplayName: displayName,
5637
+ outputSubtitle: claimedSubtitle,
5638
+ outputPrimarySurface: claimedPrimarySurface,
5292
5639
  });
5293
- (0, output_1.printSuccess)(`Claimed app slug "${claimedAppName}" for ${displayName}.`);
5640
+ (0, output_1.printSuccess)(claimedPrimarySurface
5641
+ ? `Claimed "${displayName}" (${claimedAppName}) for ${claimedPrimarySurface}.`
5642
+ : `Claimed app slug "${claimedAppName}" for ${displayName}.`);
5294
5643
  }
5295
5644
  async function completeTask(options) {
5296
5645
  const taskContext = readTaskContextFile();
@@ -5298,9 +5647,12 @@ async function completeTask(options) {
5298
5647
  throw new Error('task_done_not_allowed_for_game_review');
5299
5648
  }
5300
5649
  const summary = typeof options.summary === 'string' ? options.summary.trim() : '';
5301
- if (!summary || summary.length > 200 || /[\r\n]/.test(summary)) {
5650
+ if (!summary || /[\r\n]/.test(summary)) {
5302
5651
  throw new Error('task_done_summary_required');
5303
5652
  }
5653
+ if (summary.length > 200) {
5654
+ throw new Error('task_done_summary_too_long:max_200_characters');
5655
+ }
5304
5656
  const ctx = await resolveTaskCommandContext('task done', options.env, taskContext);
5305
5657
  if (!ctx) {
5306
5658
  return;