@playdrop/playdrop-cli 0.13.17 → 0.14.1

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.
@@ -35,7 +35,6 @@ exports.classifyWorkerEventDrainError = classifyWorkerEventDrainError;
35
35
  exports.isWorkerTransientNetworkError = isWorkerTransientNetworkError;
36
36
  exports.classifyWorkerHeartbeatError = classifyWorkerHeartbeatError;
37
37
  exports.assertWorkerClaimErrorRetryable = assertWorkerClaimErrorRetryable;
38
- exports.resolvePlaydropAssetRequirementFromText = resolvePlaydropAssetRequirementFromText;
39
38
  exports.resolvePlaywrightBrowsersPathFromExecutable = resolvePlaywrightBrowsersPathFromExecutable;
40
39
  exports.loadWorkerEnvFile = loadWorkerEnvFile;
41
40
  exports.buildWorkerCapabilities = buildWorkerCapabilities;
@@ -686,6 +685,18 @@ function normalizeWorkerInstrumentSurface(value) {
686
685
  ? value
687
686
  : null;
688
687
  }
688
+ const LEGACY_REVIEW_SURFACE_PRIORITY = [
689
+ 'MOBILE_PORTRAIT',
690
+ 'MOBILE_LANDSCAPE',
691
+ 'DESKTOP',
692
+ ];
693
+ function normalizeWorkerInstrumentSurfaceTargets(value) {
694
+ if (!Array.isArray(value))
695
+ return [];
696
+ return Array.from(new Set(value
697
+ .map(normalizeWorkerInstrumentSurface)
698
+ .filter((surface) => Boolean(surface))));
699
+ }
689
700
  function resolveWorkerInstrumentSurface(taskContext) {
690
701
  const playdrop = taskContext.metadata.playdrop;
691
702
  if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
@@ -714,7 +725,12 @@ function resolveWorkerInstrumentSurface(taskContext) {
714
725
  if (!reviewTarget || typeof reviewTarget !== 'object' || Array.isArray(reviewTarget)) {
715
726
  return null;
716
727
  }
717
- return normalizeWorkerInstrumentSurface(reviewTarget.primarySurface);
728
+ const target = reviewTarget;
729
+ const primarySurface = normalizeWorkerInstrumentSurface(target.primarySurface);
730
+ if (primarySurface)
731
+ return primarySurface;
732
+ const surfaceTargets = normalizeWorkerInstrumentSurfaceTargets(target.surfaceTargets);
733
+ return LEGACY_REVIEW_SURFACE_PRIORITY.find((surface) => surfaceTargets.includes(surface)) ?? null;
718
734
  }
719
735
  if (taskContext.kind === 'GAME_EVAL') {
720
736
  const evalTarget = metadata.evalTarget;
@@ -958,12 +974,15 @@ function normalizeTaskClaimSlugPrimarySurface(value) {
958
974
  }
959
975
  return primarySurface;
960
976
  }
961
- function readTaskNextStepsFile(filePath) {
977
+ function resolveTaskWorkspaceInputPath(filePath, workspaceDir) {
978
+ return node_path_1.default.isAbsolute(filePath) ? node_path_1.default.normalize(filePath) : node_path_1.default.resolve(workspaceDir, filePath);
979
+ }
980
+ function readTaskNextStepsFile(filePath, workspaceDir = node_process_1.default.cwd()) {
962
981
  const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
963
982
  if (!normalizedPath) {
964
983
  return undefined;
965
984
  }
966
- const resolved = node_path_1.default.resolve(normalizedPath);
985
+ const resolved = resolveTaskWorkspaceInputPath(normalizedPath, workspaceDir);
967
986
  if (!(0, node_fs_1.existsSync)(resolved)) {
968
987
  throw new Error('invalid_agent_task_next_steps_file');
969
988
  }
@@ -1038,7 +1057,7 @@ async function recordRejectedNextStepsFile(input) {
1038
1057
  }
1039
1058
  async function readTaskNextStepsFileForCommand(input) {
1040
1059
  try {
1041
- return readTaskNextStepsFile(input.filePath);
1060
+ return readTaskNextStepsFile(input.filePath, input.workspaceDir);
1042
1061
  }
1043
1062
  catch (error) {
1044
1063
  const code = error instanceof Error ? error.message : String(error);
@@ -1609,9 +1628,6 @@ function assertWorkerClaimErrorRetryable(error) {
1609
1628
  throw new Error(`worker_claim_forbidden: the server rejected this worker session (${error.message}).`);
1610
1629
  }
1611
1630
  }
1612
- function resolvePlaydropAssetRequirementFromText(value) {
1613
- return (0, upload_1.resolveWorkerPlaydropAssetRequirement)(value);
1614
- }
1615
1631
  function readCodexAuthenticated(homeDir = node_os_1.default.homedir()) {
1616
1632
  try {
1617
1633
  const parsed = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(homeDir, '.codex', 'auth.json'), 'utf8'));
@@ -2584,7 +2600,8 @@ function buildAgentFailureCode(agent, result, request) {
2584
2600
  if (providerStatus === 529 || (/\b529\b/.test(combinedOutput) && /\boverloaded?\b/i.test(combinedOutput))) {
2585
2601
  return `agent_provider_overloaded:${provider}:529`;
2586
2602
  }
2587
- if (/\boverloaded?\b/i.test(combinedOutput)) {
2603
+ if (/\boverloaded?\b/i.test(combinedOutput)
2604
+ || /\b(?:selected\s+)?model\s+is\s+at\s+capacity\b/i.test(combinedOutput)) {
2588
2605
  return `agent_provider_overloaded:${provider}`;
2589
2606
  }
2590
2607
  if (providerStatus === 429
@@ -5427,7 +5444,10 @@ async function materialTask(options) {
5427
5444
  }
5428
5445
  catch (error) {
5429
5446
  const message = error instanceof Error ? error.message : String(error);
5430
- console.warn(`Warning: task material was not shared: ${message}`);
5447
+ const suggestion = message.startsWith('task_material_asset_background_not_removed:')
5448
+ ? ' Remove the background and retry with --asset, or omit --asset if the image is honestly useful as a full frame.'
5449
+ : '';
5450
+ console.warn(`Warning: task material was not shared: ${message}.${suggestion}`);
5431
5451
  node_process_1.default.exitCode = 0;
5432
5452
  }
5433
5453
  }
@@ -5514,8 +5534,6 @@ async function uploadTask(options = {}) {
5514
5534
  expectedSubtitle: taskContext.outputSubtitle,
5515
5535
  expectedPrimarySurface: taskContext.outputPrimarySurface,
5516
5536
  remixSourceRef: taskContext.remixSourceRef ?? null,
5517
- playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5518
- creatorRequest: taskContext.creatorRequest,
5519
5537
  projectDir,
5520
5538
  creatorUsername: taskContext.creatorUsername,
5521
5539
  apiBase: ctx.envConfig.apiBase,
@@ -5555,8 +5573,6 @@ async function uploadTask(options = {}) {
5555
5573
  expectedSubtitle: taskContext.outputSubtitle,
5556
5574
  expectedPrimarySurface: taskContext.outputPrimarySurface,
5557
5575
  remixSourceRef: taskContext.remixSourceRef ?? null,
5558
- playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5559
- creatorRequest: taskContext.creatorRequest,
5560
5576
  projectDir,
5561
5577
  creatorUsername: taskContext.creatorUsername,
5562
5578
  apiBase: ctx.envConfig.apiBase,
@@ -5639,6 +5655,7 @@ async function claimSlugTask(options) {
5639
5655
  }
5640
5656
  async function completeTask(options) {
5641
5657
  const taskContext = readTaskContextFile();
5658
+ const workspaceDir = resolveTaskWorkspaceDir();
5642
5659
  if (taskContext.kind === 'GAME_REVIEW' || taskContext.kind === 'GAME_EVAL') {
5643
5660
  throw new Error('task_done_not_allowed_for_game_review');
5644
5661
  }
@@ -5664,6 +5681,7 @@ async function completeTask(options) {
5664
5681
  }
5665
5682
  const nextSteps = await readTaskNextStepsFileForCommand({
5666
5683
  filePath: options.nextSteps,
5684
+ workspaceDir,
5667
5685
  command: 'task done',
5668
5686
  ctx,
5669
5687
  taskContext,
@@ -5671,7 +5689,7 @@ async function completeTask(options) {
5671
5689
  if (nextSteps === NEXT_STEPS_READ_FAILED) {
5672
5690
  return;
5673
5691
  }
5674
- const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
5692
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile, workspaceDir);
5675
5693
  try {
5676
5694
  await ctx.client.workerCompleteAgentTask(taskContext.taskId, {
5677
5695
  taskToken: taskContext.taskToken,
@@ -5710,18 +5728,6 @@ async function completeTask(options) {
5710
5728
  }
5711
5729
  (0, output_1.printSuccess)('Task marked done.');
5712
5730
  }
5713
- function normalizeReviewSubmitState(value) {
5714
- const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
5715
- if (normalized !== 'PASSED'
5716
- && normalized !== 'GOOD'
5717
- && normalized !== 'EXCELLENT'
5718
- && normalized !== 'LOW_QUALITY'
5719
- && normalized !== 'FAILED'
5720
- && normalized !== 'ERROR') {
5721
- throw new Error('invalid_review_state');
5722
- }
5723
- return normalized;
5724
- }
5725
5731
  function readRequiredTextFile(filePath, code) {
5726
5732
  const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
5727
5733
  if (!normalizedPath) {
@@ -5749,13 +5755,13 @@ function readOptionalTextFile(filePath) {
5749
5755
  const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
5750
5756
  return content || null;
5751
5757
  }
5752
- function readPlatformFeedbackFile(filePath) {
5758
+ function readPlatformFeedbackFile(filePath, workspaceDir) {
5753
5759
  const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
5754
5760
  if (!normalizedPath) {
5755
5761
  return null;
5756
5762
  }
5757
5763
  try {
5758
- const resolved = node_path_1.default.resolve(normalizedPath);
5764
+ const resolved = resolveTaskWorkspaceInputPath(normalizedPath, workspaceDir);
5759
5765
  if (!(0, node_fs_1.existsSync)(resolved)) {
5760
5766
  throw new Error('file_missing');
5761
5767
  }
@@ -5835,27 +5841,23 @@ function resolveReviewEvidenceDir(evidenceDir) {
5835
5841
  }
5836
5842
  async function submitReviewTask(options) {
5837
5843
  const taskContext = readTaskContextFile();
5844
+ const workspaceDir = resolveTaskWorkspaceDir();
5838
5845
  if (taskContext.kind !== 'GAME_REVIEW') {
5839
5846
  throw new Error('task_submit_review_requires_game_review');
5840
5847
  }
5841
- const state = normalizeReviewSubmitState(options.state);
5848
+ const state = (0, review_1.normalizeTerminalReviewState)(options.state);
5842
5849
  const reviewMessageFile = resolveReviewFilePath(options.messageFile, 'invalid_review_message_file');
5843
- const creatorFeedbackFile = state === 'ERROR'
5844
- ? null
5845
- : resolveReviewFilePath(options.creatorFeedbackFile, 'invalid_creator_feedback_file');
5850
+ const creatorFeedbackFile = resolveReviewFilePath(options.creatorFeedbackFile, 'invalid_creator_feedback_file');
5846
5851
  const evidenceDir = resolveReviewEvidenceDir(options.evidenceDir);
5847
5852
  const reviewMessage = readRequiredTextFile(reviewMessageFile, 'invalid_review_message_file');
5848
- const creatorFeedback = state === 'ERROR'
5849
- ? null
5850
- : readOptionalTextFile(creatorFeedbackFile ?? undefined);
5851
- const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
5853
+ const creatorFeedback = readOptionalTextFile(creatorFeedbackFile);
5854
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile, workspaceDir);
5852
5855
  await (0, review_1.validateGameReviewResult)({
5853
5856
  reviewState: state,
5854
5857
  reviewMessage,
5855
5858
  creatorFeedback: creatorFeedback ?? '',
5856
5859
  evidenceDir,
5857
5860
  });
5858
- const workspaceDir = resolveTaskWorkspaceDir();
5859
5861
  const instrumentEvidence = (0, instrument_evidence_1.readAndValidateInstrumentEvidenceManifest)({ workspaceDir, evidenceDir });
5860
5862
  const evidenceFiles = readReviewEvidenceFiles(evidenceDir, workspaceDir);
5861
5863
  const ctx = await resolveTaskCommandContext('task submit-review', options.env, taskContext);
@@ -5864,6 +5866,7 @@ async function submitReviewTask(options) {
5864
5866
  }
5865
5867
  const nextSteps = await readTaskNextStepsFileForCommand({
5866
5868
  filePath: options.nextSteps,
5869
+ workspaceDir,
5867
5870
  command: 'task submit-review',
5868
5871
  ctx,
5869
5872
  taskContext,
@@ -6001,11 +6004,12 @@ async function failTask(options) {
6001
6004
  return;
6002
6005
  }
6003
6006
  const taskContext = readTaskContextFile();
6007
+ const workspaceDir = resolveTaskWorkspaceDir();
6004
6008
  const ctx = await resolveTaskCommandContext('task fail', options.env, taskContext);
6005
6009
  if (!ctx) {
6006
6010
  return;
6007
6011
  }
6008
- const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
6012
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile, workspaceDir);
6009
6013
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
6010
6014
  taskToken: taskContext.taskToken,
6011
6015
  error: message,
package/dist/index.js CHANGED
@@ -289,7 +289,7 @@ task
289
289
  task
290
290
  .command('submit-review')
291
291
  .description('Submit the active game review task result')
292
- .requiredOption('--state <state>', 'Terminal review state')
292
+ .requiredOption('--state <state>', 'Review outcome: Blocked, Limited, Passed, Good, or Excellent')
293
293
  .requiredOption('--message-file <path>', 'Internal assessment file')
294
294
  .option('--creator-feedback-file <path>', 'Creator feedback file')
295
295
  .option('--platform-feedback-file <path>', 'Markdown feedback about the PlayDrop platform')
@@ -339,7 +339,7 @@ const review = program.command('review').description('Review helper commands for
339
339
  review
340
340
  .command('validate-result')
341
341
  .description('Validate a canonical game review result')
342
- .requiredOption('--state <state>', 'Terminal review state')
342
+ .requiredOption('--state <state>', 'Review outcome: Blocked, Limited, Passed, Good, or Excellent')
343
343
  .requiredOption('--message-file <path>', 'Internal assessment file')
344
344
  .option('--creator-feedback-file <path>', 'Creator feedback file')
345
345
  .requiredOption('--evidence-dir <path>', 'Review evidence directory')
@@ -1038,14 +1038,12 @@ project
1038
1038
  .option('--app <name>', 'App name when the target is a workspace')
1039
1039
  .option('--timeout <seconds>', 'Load-check timeout in seconds')
1040
1040
  .option('--screenshot <path>', 'Screenshot output path')
1041
- .option('--actions <path>', 'JSON array of click {x,y}, press {key}, and wait {ms <= 60000} actions')
1042
1041
  .option('--tape <surface>', 'Run matched idle and catalogue playtest-tape captures on a declared surface')
1043
1042
  .action(async (target, opts = {}) => {
1044
1043
  await (0, check_1.check)(target, {
1045
1044
  app: opts.app,
1046
1045
  timeout: opts.timeout,
1047
1046
  screenshot: opts.screenshot,
1048
- actions: opts.actions,
1049
1047
  tape: opts.tape,
1050
1048
  });
1051
1049
  });
@@ -17,6 +17,9 @@ export declare function readMediaCaptureProof(input: {
17
17
  }): RecordAppUploadMediaCaptureRequest;
18
18
  export declare function readWorkerUploadPreflightContext(projectDir: string): WorkerUploadPreflightContext | null;
19
19
  export declare function assertAgentGameHeroListingPreflight(task: AppTask): void;
20
+ export declare function assertAgentGameIconListingPreflight(task: AppTask): void;
21
+ export declare function assertAgentGameVideoListingPreflight(task: AppTask): void;
22
+ export declare function assertAgentGamePreviewListingPreflight(task: AppTask): void;
20
23
  export declare function assertAgentGameScreenshotListingPreflight(task: AppTask): void;
21
24
  export declare function assertAgentGameCaptureReportPreflight(task: AppTask, options: {
22
25
  requireCaptureReport: boolean;
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readMediaCaptureProof = readMediaCaptureProof;
4
4
  exports.readWorkerUploadPreflightContext = readWorkerUploadPreflightContext;
5
5
  exports.assertAgentGameHeroListingPreflight = assertAgentGameHeroListingPreflight;
6
+ exports.assertAgentGameIconListingPreflight = assertAgentGameIconListingPreflight;
7
+ exports.assertAgentGameVideoListingPreflight = assertAgentGameVideoListingPreflight;
8
+ exports.assertAgentGamePreviewListingPreflight = assertAgentGamePreviewListingPreflight;
6
9
  exports.assertAgentGameScreenshotListingPreflight = assertAgentGameScreenshotListingPreflight;
7
10
  exports.assertAgentGameCaptureReportPreflight = assertAgentGameCaptureReportPreflight;
8
11
  exports.assertAgentGameListingPreflight = assertAgentGameListingPreflight;
@@ -37,9 +40,12 @@ function readPngDimensions(filePath, errorCode = 'agent_task_listing_hero_art_in
37
40
  }
38
41
  return { width, height };
39
42
  }
40
- function assertListingIcon(task) {
43
+ function assertListingIcon(task, required = false) {
41
44
  const iconPath = task.listing?.iconPath;
42
45
  if (!iconPath) {
46
+ if (required) {
47
+ throw new Error('agent_task_listing_icon_missing: agent game listings must include a dedicated square PNG through listing.icon.');
48
+ }
43
49
  return;
44
50
  }
45
51
  const icon = readPngDimensions(iconPath, 'listing_icon_invalid');
@@ -116,15 +122,6 @@ function assertScreenshotPath(task, filePath, field) {
116
122
  function assertListingScreenshots(task) {
117
123
  const portraitPaths = task.listing?.screenshotPortraitPaths ?? [];
118
124
  const landscapePaths = task.listing?.screenshotLandscapePaths ?? [];
119
- if (portraitPaths.length === 0 && landscapePaths.length === 0) {
120
- throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG captures.');
121
- }
122
- if (task.surfaceTargets.includes('MOBILE_PORTRAIT') && portraitPaths.length === 0) {
123
- throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG capture.');
124
- }
125
- if ((task.surfaceTargets.includes('DESKTOP') || task.surfaceTargets.includes('MOBILE_LANDSCAPE')) && landscapePaths.length === 0) {
126
- throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG capture.');
127
- }
128
125
  for (const filePath of portraitPaths) {
129
126
  assertScreenshotPath(task, filePath, 'listing.screenshotsPortrait');
130
127
  readPngDimensions(filePath);
@@ -134,13 +131,23 @@ function assertListingScreenshots(task) {
134
131
  readPngDimensions(filePath);
135
132
  }
136
133
  }
134
+ function assertListingGameplayVideo(task) {
135
+ const videoCount = (task.listing?.videoPortraitPaths?.length ?? 0)
136
+ + (task.listing?.videoLandscapePaths?.length ?? 0);
137
+ if (videoCount === 0) {
138
+ throw new Error('agent_task_listing_video_missing: agent game listings must include at least one real gameplay video through listing.videosPortrait or listing.videosLandscape.');
139
+ }
140
+ }
141
+ function assertListingPreviewMode(task) {
142
+ if (task.previewable !== true) {
143
+ throw new Error('agent_task_listing_preview_missing: agent game listings must set previewable: true and implement the real gameplay preview contract before capture.');
144
+ }
145
+ }
137
146
  function isSha256Hex(value) {
138
147
  return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
139
148
  }
140
- function isCaptureMediaFileKey(fileKey) {
141
- return fileKey.startsWith('screenshotsPortrait:')
142
- || fileKey.startsWith('screenshotsLandscape:')
143
- || fileKey.startsWith('videosPortrait:')
149
+ function isCaptureVideoFileKey(fileKey) {
150
+ return fileKey.startsWith('videosPortrait:')
144
151
  || fileKey.startsWith('videosLandscape:');
145
152
  }
146
153
  function readMediaCaptureProof(input) {
@@ -160,6 +167,12 @@ function readMediaCaptureProof(input) {
160
167
  if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
161
168
  throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} is missing binding.`);
162
169
  }
170
+ if (report.appName !== input.task.name) {
171
+ throw new Error(`agent_task_media_capture_wrong_app: listing.captureReport is for ${String(report.appName || 'unknown')}, not ${input.task.name}; rerun "playdrop project capture".`);
172
+ }
173
+ if (binding.pixelValidation !== 'passed') {
174
+ throw new Error(`agent_task_media_capture_pixels_unvalidated: ${reportPath} must come from a capture that passed wrong-game, black-frame, and motion checks.`);
175
+ }
163
176
  if (input.expectedRuntimeBundleHash) {
164
177
  const capturedRuntimeBundleHash = typeof binding.runtimeBundleHash === 'string'
165
178
  ? binding.runtimeBundleHash.trim().toLowerCase()
@@ -175,20 +188,21 @@ function readMediaCaptureProof(input) {
175
188
  if (!rawArtifactHashes || typeof rawArtifactHashes !== 'object' || Array.isArray(rawArtifactHashes)) {
176
189
  throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must be an object.`);
177
190
  }
178
- const captureArtifactHashEntries = Object.values(rawArtifactHashes)
179
- .map((value) => typeof value === 'string' ? value.trim().toLowerCase() : '')
191
+ const captureArtifactHashEntries = Object.entries(rawArtifactHashes)
192
+ .filter(([artifactKey]) => (0, node_path_1.extname)(artifactKey).toLowerCase() === '.mp4')
193
+ .map(([, value]) => typeof value === 'string' ? value.trim().toLowerCase() : '')
180
194
  .filter((value) => isSha256Hex(value));
181
195
  const captureArtifactHashes = new Set(captureArtifactHashEntries);
182
196
  if (captureArtifactHashEntries.length === 0) {
183
- throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes is empty.`);
197
+ throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must include at least one MP4 gameplay video.`);
184
198
  }
185
199
  const captureArtifactHashCounts = new Map();
186
200
  for (const hash of captureArtifactHashEntries) {
187
201
  captureArtifactHashCounts.set(hash, (captureArtifactHashCounts.get(hash) ?? 0) + 1);
188
202
  }
189
- const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureMediaFileKey(file.fileKey));
203
+ const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureVideoFileKey(file.fileKey));
190
204
  if (mediaFiles.length === 0) {
191
- throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder stills and video in listing.screenshots* and listing.videos*.`);
205
+ throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder gameplay video in listing.videos*.`);
192
206
  }
193
207
  const uploadedMediaHashCounts = new Map();
194
208
  for (const file of mediaFiles) {
@@ -197,21 +211,19 @@ function readMediaCaptureProof(input) {
197
211
  const missingUploadedCaptureArtifacts = [...captureArtifactHashCounts.entries()]
198
212
  .filter(([hash, expectedCount]) => (uploadedMediaHashCounts.get(hash) ?? 0) < expectedCount);
199
213
  if (missingUploadedCaptureArtifacts.length > 0) {
200
- throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder poster and video from listing.captureReport in catalogue.json.`);
214
+ throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder gameplay video from listing.captureReport in catalogue.json.`);
201
215
  }
202
- let hasStill = false;
203
216
  let hasVideo = false;
204
217
  const artifactHashes = {};
205
218
  for (const file of mediaFiles) {
206
219
  if (!captureArtifactHashes.has(file.sha256)) {
207
220
  throw new Error(`agent_task_media_capture_artifact_mismatch: ${file.fileKey} was not produced by listing.captureReport for ${input.task.name}.`);
208
221
  }
209
- hasStill = hasStill || file.fileKey.startsWith('screenshots');
210
222
  hasVideo = hasVideo || file.fileKey.startsWith('videos');
211
223
  artifactHashes[file.fileKey] = file.sha256;
212
224
  }
213
- if (!hasStill || !hasVideo) {
214
- throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder still and one recorder video.`);
225
+ if (!hasVideo) {
226
+ throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder gameplay video.`);
215
227
  }
216
228
  const capturedSurfaces = Array.isArray(binding.capturedSurfaces)
217
229
  ? Array.from(new Set(binding.capturedSurfaces
@@ -312,6 +324,15 @@ function readWorkerUploadPreflightContext(projectDir) {
312
324
  function assertAgentGameHeroListingPreflight(task) {
313
325
  assertHeroListingArt(task);
314
326
  }
327
+ function assertAgentGameIconListingPreflight(task) {
328
+ assertListingIcon(task, true);
329
+ }
330
+ function assertAgentGameVideoListingPreflight(task) {
331
+ assertListingGameplayVideo(task);
332
+ }
333
+ function assertAgentGamePreviewListingPreflight(task) {
334
+ assertListingPreviewMode(task);
335
+ }
315
336
  function assertAgentGameScreenshotListingPreflight(task) {
316
337
  assertListingScreenshots(task);
317
338
  }
@@ -325,7 +346,10 @@ function assertAgentGameCaptureReportPreflight(task, options) {
325
346
  }
326
347
  }
327
348
  function assertAgentGameListingPreflight(task, options) {
349
+ assertAgentGameIconListingPreflight(task);
328
350
  assertAgentGameHeroListingPreflight(task);
351
+ assertAgentGamePreviewListingPreflight(task);
352
+ assertAgentGameVideoListingPreflight(task);
329
353
  assertAgentGameScreenshotListingPreflight(task);
330
354
  assertAgentGameCaptureReportPreflight(task, options);
331
355
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.13.17",
2
+ "version": "0.14.1",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.13.17",
4
+ "runtimeSdkVersion": "0.14.1",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -1 +1 @@
1
- {"version":3,"file":"app-playtest-tape.d.ts","sourceRoot":"","sources":["../src/app-playtest-tape.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,UAAU,CAAC;AAElB,eAAO,MAAM,yBAAyB,EAAG,CAAU,CAAC;AACpD,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAEvD,eAAO,MAAM,gCAAgC,0CAA2C,CAAC;AACzF,MAAM,MAAM,sBAAsB,GAAG,OAAO,gCAAgC,CAAC,MAAM,CAAC,CAAC;AASrF,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAEtG;AAED,eAAO,MAAM,uCAAuC,sFAK1C,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAAG,OAAO,uCAAuC,CAAC,MAAM,CAAC,CAAC;AAElG,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,4BAA4B,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,qBAAqB,GAAG;IAC5D,IAAI,EAAE,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;IAClD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG;IACxD,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,GAAG;IAC7D,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAC5B,uBAAuB,GACvB,mBAAmB,GACnB,wBAAwB,CAAC;AAE7B,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,OAAO,yBAAyB,CAAC;IAC1C,WAAW,EAAE,sBAAsB,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,wBAAwB,EAAE,CAAC;IAC3C,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC;AAE5E,MAAM,MAAM,+BAA+B,GACvC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA0PjC,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,+BAA+B,CAoCjC"}
1
+ {"version":3,"file":"app-playtest-tape.d.ts","sourceRoot":"","sources":["../src/app-playtest-tape.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,UAAU,CAAC;AAElB,eAAO,MAAM,yBAAyB,EAAG,CAAU,CAAC;AACpD,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAEvD,eAAO,MAAM,gCAAgC,0CAA2C,CAAC;AACzF,MAAM,MAAM,sBAAsB,GAAG,OAAO,gCAAgC,CAAC,MAAM,CAAC,CAAC;AASrF,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAEtG;AAED,eAAO,MAAM,uCAAuC,sFAK1C,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAAG,OAAO,uCAAuC,CAAC,MAAM,CAAC,CAAC;AAElG,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,4BAA4B,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,qBAAqB,GAAG;IAC5D,IAAI,EAAE,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;IAClD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG;IACxD,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,GAAG;IAC7D,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAC5B,uBAAuB,GACvB,mBAAmB,GACnB,wBAAwB,CAAC;AAE7B,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,OAAO,yBAAyB,CAAC;IAC1C,WAAW,EAAE,sBAAsB,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,wBAAwB,EAAE,CAAC;IAC3C,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC;AAE5E,MAAM,MAAM,+BAA+B,GACvC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAgQjC,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,+BAA+B,CAoCjC"}
@@ -106,7 +106,8 @@ function normalizeEvent(value, surface, durationMs, index) {
106
106
  if (surface !== "DESKTOP") {
107
107
  throw new Error(`keyboard_input_not_allowed_on_mobile_tape:${surface}:${index}`);
108
108
  }
109
- const key = typeof value["key"] === "string" ? value["key"].trim() : "";
109
+ const rawKey = typeof value["key"] === "string" ? value["key"] : "";
110
+ const key = rawKey === " " ? rawKey : rawKey.trim();
110
111
  if (!key || key.length > 64) {
111
112
  throw new Error(`invalid_playtest_tape_key:${surface}:${index}`);
112
113
  }
@@ -242,30 +243,37 @@ function normalizeTape(value, surface) {
242
243
  if (primaryVerb === "tap")
243
244
  return event.type === "tap";
244
245
  if (primaryVerb === "key")
245
- return event.type === "keyDown" || event.type === "keyUp";
246
+ return event.type === "keyDown";
246
247
  return event.type === "pointerDown" || event.type === "pointerMove" || event.type === "pointerUp";
247
248
  };
248
- const mismatchedEventIndex = postStartEvents.findIndex((event) => !postStartEventMatchesVerb(event));
249
- if (mismatchedEventIndex >= 0) {
250
- throw new Error(`playtest_tape_primary_verb_event_mismatch:${surface}:${primaryVerb}:${startOnlyEventCount + mismatchedEventIndex}`);
249
+ if (!postStartEvents.some(postStartEventMatchesVerb)) {
250
+ throw new Error(`playtest_tape_primary_verb_events_required:${surface}:${primaryVerb}`);
251
251
  }
252
252
  if (primaryVerb === "swipe" || primaryVerb === "drag") {
253
- let actionHasMove = false;
254
- let actionOpen = false;
255
- for (let index = 0; index < postStartEvents.length; index += 1) {
256
- const event = postStartEvents[index];
253
+ const openGestures = /* @__PURE__ */ new Set();
254
+ const movedGestures = /* @__PURE__ */ new Set();
255
+ let completePrimaryGesture = false;
256
+ for (const event of postStartEvents) {
257
+ if (event.type !== "pointerDown" && event.type !== "pointerMove" && event.type !== "pointerUp")
258
+ continue;
259
+ const pointerId = event.pointerId ?? 0;
257
260
  if (event.type === "pointerDown") {
258
- actionOpen = true;
259
- actionHasMove = false;
261
+ openGestures.add(pointerId);
262
+ movedGestures.delete(pointerId);
260
263
  } else if (event.type === "pointerMove") {
261
- actionHasMove = true;
264
+ if (openGestures.has(pointerId))
265
+ movedGestures.add(pointerId);
262
266
  } else if (event.type === "pointerUp") {
263
- if (!actionOpen || !actionHasMove) {
264
- throw new Error(`playtest_tape_primary_verb_gesture_incomplete:${surface}:${primaryVerb}:${startOnlyEventCount + index}`);
267
+ if (openGestures.has(pointerId) && movedGestures.has(pointerId)) {
268
+ completePrimaryGesture = true;
265
269
  }
266
- actionOpen = false;
270
+ openGestures.delete(pointerId);
271
+ movedGestures.delete(pointerId);
267
272
  }
268
273
  }
274
+ if (!completePrimaryGesture) {
275
+ throw new Error(`playtest_tape_primary_verb_gesture_required:${surface}:${primaryVerb}`);
276
+ }
269
277
  }
270
278
  return {
271
279
  version: APP_PLAYTEST_TAPE_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playdrop/playdrop-cli",
3
- "version": "0.13.17",
3
+ "version": "0.14.1",
4
4
  "description": "Official Playdrop CLI for publishing browser games, creator apps, and AI-generated game assets on playdrop.ai",
5
5
  "homepage": "https://www.playdrop.ai",
6
6
  "repository": {