@playdrop/playdrop-cli 0.12.26 → 0.12.28

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 (38) hide show
  1. package/config/client-meta.json +1 -1
  2. package/dist/apps/build.js +43 -0
  3. package/dist/apps/index.d.ts +2 -1
  4. package/dist/apps/index.js +1 -0
  5. package/dist/apps/upload.d.ts +2 -1
  6. package/dist/apps/upload.js +1 -0
  7. package/dist/commands/devRuntimeAssets.js +26 -124
  8. package/dist/commands/ownedRuntimeImageValidation.d.ts +7 -0
  9. package/dist/commands/ownedRuntimeImageValidation.js +76 -0
  10. package/dist/commands/upload.d.ts +7 -1
  11. package/dist/commands/upload.js +79 -402
  12. package/dist/commands/worker.d.ts +16 -0
  13. package/dist/commands/worker.js +164 -35
  14. package/dist/listingPreflight.d.ts +1 -0
  15. package/dist/listingPreflight.js +17 -0
  16. package/node_modules/@playdrop/api-client/dist/client.d.ts +2 -1
  17. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  18. package/node_modules/@playdrop/api-client/dist/core/errors.d.ts.map +1 -1
  19. package/node_modules/@playdrop/api-client/dist/core/errors.js +3 -1
  20. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  21. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  22. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
  23. package/node_modules/@playdrop/api-client/dist/index.d.ts +2 -1
  24. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/index.js +5 -0
  26. package/node_modules/@playdrop/config/client-meta.json +1 -1
  27. package/node_modules/@playdrop/types/dist/agent-task-playtest.d.ts +23 -0
  28. package/node_modules/@playdrop/types/dist/agent-task-playtest.d.ts.map +1 -0
  29. package/node_modules/@playdrop/types/dist/agent-task-playtest.js +84 -0
  30. package/node_modules/@playdrop/types/dist/api.d.ts +13 -0
  31. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  32. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  33. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  34. package/node_modules/@playdrop/types/dist/index.js +3 -1
  35. package/node_modules/@playdrop/types/dist/version.d.ts +4 -2
  36. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  37. package/node_modules/@playdrop/types/dist/version.js +13 -3
  38. package/package.json +1 -1
@@ -1,40 +1,11 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveUploadOwner = resolveUploadOwner;
4
+ exports.resolveDefaultUploadCreator = resolveDefaultUploadCreator;
36
5
  exports.upload = upload;
6
+ exports.assertTaskPlaytestEvidenceManifest = assertTaskPlaytestEvidenceManifest;
37
7
  exports.publishWorkerAppProject = publishWorkerAppProject;
8
+ const node_crypto_1 = require("node:crypto");
38
9
  const node_fs_1 = require("node:fs");
39
10
  const node_path_1 = require("node:path");
40
11
  const semver_1 = require("semver");
@@ -51,6 +22,7 @@ const uploadLog_1 = require("../uploadLog");
51
22
  const upload_content_1 = require("./upload-content");
52
23
  const appUrls_1 = require("../appUrls");
53
24
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
25
+ const ownedRuntimeImageValidation_1 = require("./ownedRuntimeImageValidation");
54
26
  const listingPreflight_1 = require("../listingPreflight");
55
27
  const upload_graph_1 = require("./upload-graph");
56
28
  function normalizeSavedItemKinds(kinds) {
@@ -220,6 +192,19 @@ function normalizeCreatorUsername(username) {
220
192
  }
221
193
  return trimmed;
222
194
  }
195
+ function resolveUploadOwner(actorUsername, workspaceOwnerUsername) {
196
+ const workspaceOwner = workspaceOwnerUsername?.trim() ?? '';
197
+ if (workspaceOwner) {
198
+ return workspaceOwner;
199
+ }
200
+ const actor = actorUsername?.trim() ?? '';
201
+ return actor || 'unknown';
202
+ }
203
+ function resolveDefaultUploadCreator(owner, actorUsername) {
204
+ return normalizeCreatorUsername(owner ?? null)
205
+ ?? normalizeCreatorUsername(actorUsername ?? null)
206
+ ?? 'unknown';
207
+ }
223
208
  function isNotFoundApiError(error) {
224
209
  return error instanceof types_1.ApiError && error.status === 404;
225
210
  }
@@ -1057,7 +1042,7 @@ async function flushGraphState(client, graphState, results) {
1057
1042
  }
1058
1043
  async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, localTagGroups, currentUser, token, warnings, apiBase, webBase, options) {
1059
1044
  const portalBase = normalizePortalBase(webBase);
1060
- const defaultCreator = normalizeCreatorUsername(ownerUsername) ?? owner;
1045
+ const defaultCreator = resolveDefaultUploadCreator(owner, ownerUsername);
1061
1046
  const results = [];
1062
1047
  const preflight = await buildUploadPreflight(client, tasks, defaultCreator, currentUserRole, localTagGroups);
1063
1048
  const sortedTasks = preflight.nodes.map((node) => node.task);
@@ -1175,9 +1160,7 @@ async function upload(pathOrName, options) {
1175
1160
  }
1176
1161
  throw error;
1177
1162
  }
1178
- const owner = typeof userInfo.username === 'string' && userInfo.username.trim().length > 0
1179
- ? userInfo.username.trim()
1180
- : 'unknown';
1163
+ const owner = resolveUploadOwner(userInfo.username, ctx.workspaceAuth?.config.ownerUsername);
1181
1164
  const tagGroupLoad = (0, catalogue_1.resolveCatalogueTagGroups)(workspaceRoot);
1182
1165
  if (tagGroupLoad.errors.length > 0) {
1183
1166
  (0, taskSelection_1.reportTaskErrors)(tagGroupLoad.errors.map((message) => ({
@@ -1232,8 +1215,6 @@ const WORKER_SUPERVISOR_BUNDLE_EXCLUDES = [
1232
1215
  'assets/marketing/',
1233
1216
  'listing/',
1234
1217
  ];
1235
- const OWNED_RUNTIME_IMAGE_CHECK_SIZE = 96;
1236
- const OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE = 512;
1237
1218
  function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
1238
1219
  if (!requirement) {
1239
1220
  return true;
@@ -1251,361 +1232,6 @@ function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
1251
1232
  });
1252
1233
  return hasFirstPartyPack || hasFirstPartyAsset || task.ownedAssets.length > 0;
1253
1234
  }
1254
- function isUnsafeDirectPlaydropAiImageAsset(detail, parsedRevision) {
1255
- const asset = detail?.asset;
1256
- if (!asset || asset.category !== 'IMAGE') {
1257
- return false;
1258
- }
1259
- const currentVersion = asset.currentVersion;
1260
- if (!currentVersion || Number(currentVersion.revision) !== parsedRevision) {
1261
- return false;
1262
- }
1263
- const sourceKind = typeof currentVersion.sourceKind === 'string' ? currentVersion.sourceKind.toUpperCase() : '';
1264
- if (sourceKind !== 'AI') {
1265
- return false;
1266
- }
1267
- const format = typeof currentVersion.format === 'string' ? currentVersion.format.toUpperCase() : '';
1268
- const files = Array.isArray(currentVersion.fileManifest?.files) ? currentVersion.fileManifest.files : [];
1269
- const primary = files.find((file) => file?.role === 'primary');
1270
- const primaryContentType = typeof primary?.contentType === 'string' ? primary.contentType.toLowerCase() : '';
1271
- return format === 'JPEG' || primaryContentType === 'image/jpeg' || primaryContentType === 'image/jpg';
1272
- }
1273
- async function assertNewGameDirectPlaydropAssetsAreRuntimeSafe(input) {
1274
- const unsafeRefs = [];
1275
- for (const dependency of input.task.uses.assets) {
1276
- const parsed = (0, types_1.parseContentVersionRef)(dependency.ref);
1277
- if (!parsed || parsed.kind !== 'asset' || parsed.creatorUsername.toLowerCase() !== 'playdrop') {
1278
- continue;
1279
- }
1280
- const detail = await input.client.fetchAssetBySlug(parsed.creatorUsername, parsed.name);
1281
- if (isUnsafeDirectPlaydropAiImageAsset(detail, parsed.revision)) {
1282
- unsafeRefs.push((0, types_1.formatContentVersionRef)(parsed));
1283
- }
1284
- }
1285
- if (unsafeRefs.length > 0) {
1286
- throw new Error(`agent_task_playdrop_direct_ai_image_asset_unsafe: direct PlayDrop AI JPEG image assets cannot be used as new-game runtime sprites because their backgrounds are baked into the image. Use a PlayDrop asset pack or create clean owned PNG assets through the PlayDrop make-assets workflow instead. Unsafe refs: ${unsafeRefs.join(', ')}`);
1287
- }
1288
- }
1289
- function isOwnedRuntimeImageAsset(task, ownedAsset) {
1290
- if (!ownedAsset.runtimeKey || ownedAsset.category !== 'IMAGE') {
1291
- return false;
1292
- }
1293
- const primaryPath = ownedAsset.filePaths?.primary;
1294
- if (!primaryPath) {
1295
- return false;
1296
- }
1297
- const relativePath = primaryPath.slice((0, node_path_1.resolve)(task.projectDir).length + 1).replace(/\\/g, '/');
1298
- return !relativePath.startsWith('assets/marketing/') && !relativePath.startsWith('listing/');
1299
- }
1300
- function isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset) {
1301
- const value = `${ownedAsset.name ?? ''} ${ownedAsset.runtimeKey ?? ''}`.toLowerCase();
1302
- return /\b(background|backdrop|scene|sky|floor|ground|wall|terrain|landscape|level|arena)\b/i.test(value);
1303
- }
1304
- function looksLikeBakedCheckerboardPreview(input) {
1305
- const { data, width, height, channels } = input;
1306
- if (width < 24 || height < 24 || channels < 4) {
1307
- return false;
1308
- }
1309
- const border = Math.max(8, Math.floor(Math.min(width, height) * 0.16));
1310
- let sampled = 0;
1311
- let transparent = 0;
1312
- let opaqueGray = 0;
1313
- let lowGray = 0;
1314
- let highGray = 0;
1315
- for (let y = 0; y < height; y += 1) {
1316
- for (let x = 0; x < width; x += 1) {
1317
- const inBorder = x < border || y < border || x >= width - border || y >= height - border;
1318
- if (!inBorder) {
1319
- continue;
1320
- }
1321
- sampled += 1;
1322
- const offset = (y * width + x) * channels;
1323
- const red = data[offset] ?? 0;
1324
- const green = data[offset + 1] ?? 0;
1325
- const blue = data[offset + 2] ?? 0;
1326
- const alpha = data[offset + 3] ?? 255;
1327
- if (alpha < 32) {
1328
- transparent += 1;
1329
- continue;
1330
- }
1331
- const brightness = (red + green + blue) / 3;
1332
- const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 10;
1333
- if (alpha > 240 && grayish && brightness >= 70 && brightness <= 210) {
1334
- opaqueGray += 1;
1335
- if (brightness < 125) {
1336
- lowGray += 1;
1337
- }
1338
- else if (brightness > 145) {
1339
- highGray += 1;
1340
- }
1341
- }
1342
- }
1343
- }
1344
- if (sampled === 0 || transparent / sampled > 0.18) {
1345
- return false;
1346
- }
1347
- return opaqueGray / sampled > 0.58
1348
- && lowGray / sampled > 0.08
1349
- && highGray / sampled > 0.18;
1350
- }
1351
- function looksLikeBakedSolidMattePreview(input) {
1352
- const { data, width, height, channels } = input;
1353
- if (width < 24 || height < 24 || channels < 4) {
1354
- return false;
1355
- }
1356
- const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
1357
- let sampled = 0;
1358
- let transparent = 0;
1359
- let matteLike = 0;
1360
- for (let y = 0; y < height; y += 1) {
1361
- for (let x = 0; x < width; x += 1) {
1362
- const inBorder = x < border || y < border || x >= width - border || y >= height - border;
1363
- if (!inBorder) {
1364
- continue;
1365
- }
1366
- sampled += 1;
1367
- const offset = (y * width + x) * channels;
1368
- const red = data[offset] ?? 0;
1369
- const green = data[offset + 1] ?? 0;
1370
- const blue = data[offset + 2] ?? 0;
1371
- const alpha = data[offset + 3] ?? 255;
1372
- if (alpha < 32) {
1373
- transparent += 1;
1374
- continue;
1375
- }
1376
- const dominant = Math.max(red, green, blue);
1377
- const weakest = Math.min(red, green, blue);
1378
- const chroma = dominant - weakest;
1379
- const brightGreenMatte = green >= 220 && green - Math.max(red, blue) >= 65 && Math.max(red, blue) >= 70;
1380
- const brightRedMatte = red >= 220 && red - Math.max(green, blue) >= 65;
1381
- const brightBlueMatte = blue >= 220 && blue - Math.max(red, green) >= 65;
1382
- const brightTwoChannelMatte = dominant >= 220
1383
- && chroma >= 65
1384
- && ((red >= 220 && green >= 220) || (red >= 220 && blue >= 220) || (green >= 220 && blue >= 220));
1385
- if (brightGreenMatte || brightRedMatte || brightBlueMatte || brightTwoChannelMatte) {
1386
- matteLike += 1;
1387
- }
1388
- }
1389
- }
1390
- if (sampled === 0 || transparent / sampled > 0.18) {
1391
- return false;
1392
- }
1393
- return matteLike / sampled > 0.58;
1394
- }
1395
- function looksLikeBakedNeutralSolidMattePreview(input) {
1396
- const { data, width, height, channels } = input;
1397
- if (width < 24 || height < 24 || channels < 4) {
1398
- return false;
1399
- }
1400
- const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
1401
- let sampled = 0;
1402
- let transparent = 0;
1403
- let neutralMatteLike = 0;
1404
- for (let y = 0; y < height; y += 1) {
1405
- for (let x = 0; x < width; x += 1) {
1406
- const inBorder = x < border || y < border || x >= width - border || y >= height - border;
1407
- if (!inBorder) {
1408
- continue;
1409
- }
1410
- sampled += 1;
1411
- const offset = (y * width + x) * channels;
1412
- const red = data[offset] ?? 0;
1413
- const green = data[offset + 1] ?? 0;
1414
- const blue = data[offset + 2] ?? 0;
1415
- const alpha = data[offset + 3] ?? 255;
1416
- if (alpha < 32) {
1417
- transparent += 1;
1418
- continue;
1419
- }
1420
- const brightness = (red + green + blue) / 3;
1421
- const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 12;
1422
- if (alpha > 240 && grayish && (brightness >= 235 || brightness <= 20)) {
1423
- neutralMatteLike += 1;
1424
- }
1425
- }
1426
- }
1427
- if (sampled === 0 || transparent / sampled > 0.18) {
1428
- return false;
1429
- }
1430
- return neutralMatteLike / sampled > 0.58;
1431
- }
1432
- function looksLikeResidualMatteGridArtifact(input) {
1433
- const { data, width, height, channels } = input;
1434
- if (width < 48 || height < 48 || channels < 4) {
1435
- return false;
1436
- }
1437
- const rowCounts = new Uint32Array(height);
1438
- const columnCounts = new Uint32Array(width);
1439
- let matteLike = 0;
1440
- for (let y = 0; y < height; y += 1) {
1441
- for (let x = 0; x < width; x += 1) {
1442
- const offset = (y * width + x) * channels;
1443
- const red = data[offset] ?? 0;
1444
- const green = data[offset + 1] ?? 0;
1445
- const blue = data[offset + 2] ?? 0;
1446
- const alpha = data[offset + 3] ?? 255;
1447
- if (alpha < 24 || alpha > 190) {
1448
- continue;
1449
- }
1450
- const greenMatte = green >= 35 && green - Math.max(red, blue) >= 25 && red <= 110 && blue <= 110;
1451
- const redMatte = red >= 35 && red - Math.max(green, blue) >= 25 && green <= 120 && blue <= 120;
1452
- const blueMatte = blue >= 35 && blue - Math.max(red, green) >= 25 && red <= 120 && green <= 120;
1453
- const purpleMatte = red >= 35 && blue >= 35 && Math.min(red, blue) - green >= 25 && green <= 120;
1454
- if (!greenMatte && !redMatte && !blueMatte && !purpleMatte) {
1455
- continue;
1456
- }
1457
- matteLike += 1;
1458
- rowCounts[y] += 1;
1459
- columnCounts[x] += 1;
1460
- }
1461
- }
1462
- const matteRatio = matteLike / (width * height);
1463
- if (matteRatio < 0.0015) {
1464
- return false;
1465
- }
1466
- let rowsWithLines = 0;
1467
- for (const count of rowCounts) {
1468
- if (count / width >= 0.12) {
1469
- rowsWithLines += 1;
1470
- }
1471
- }
1472
- let columnsWithLines = 0;
1473
- for (const count of columnCounts) {
1474
- if (count / height >= 0.12) {
1475
- columnsWithLines += 1;
1476
- }
1477
- }
1478
- return rowsWithLines + columnsWithLines >= 3
1479
- && (rowsWithLines >= 2 || columnsWithLines >= 2);
1480
- }
1481
- function looksLikeResidualTransparentGridLineArtifact(input) {
1482
- const { data, width, height, channels } = input;
1483
- if (width < 48 || height < 48 || channels < 4) {
1484
- return false;
1485
- }
1486
- const rowVisibleCounts = new Uint32Array(height);
1487
- const rowSemiCounts = new Uint32Array(height);
1488
- const columnVisibleCounts = new Uint32Array(width);
1489
- const columnSemiCounts = new Uint32Array(width);
1490
- for (let y = 0; y < height; y += 1) {
1491
- for (let x = 0; x < width; x += 1) {
1492
- const offset = (y * width + x) * channels;
1493
- const alpha = data[offset + 3] ?? 255;
1494
- if (alpha <= 0) {
1495
- continue;
1496
- }
1497
- rowVisibleCounts[y] += 1;
1498
- columnVisibleCounts[x] += 1;
1499
- if (alpha < 255) {
1500
- rowSemiCounts[y] += 1;
1501
- columnSemiCounts[x] += 1;
1502
- }
1503
- }
1504
- }
1505
- let rowsWithLines = 0;
1506
- for (let row = 0; row < height; row += 1) {
1507
- const visibleRatio = rowVisibleCounts[row] / width;
1508
- const semiRatio = rowSemiCounts[row] / width;
1509
- if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
1510
- rowsWithLines += 1;
1511
- }
1512
- }
1513
- let columnsWithLines = 0;
1514
- for (let column = 0; column < width; column += 1) {
1515
- const visibleRatio = columnVisibleCounts[column] / height;
1516
- const semiRatio = columnSemiCounts[column] / height;
1517
- if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
1518
- columnsWithLines += 1;
1519
- }
1520
- }
1521
- return rowsWithLines + columnsWithLines >= 3
1522
- && (rowsWithLines >= 2 || columnsWithLines >= 2);
1523
- }
1524
- async function imageHasBakedPreviewBackground(filePath, options = {}) {
1525
- let sharp;
1526
- try {
1527
- const sharpModule = await Promise.resolve().then(() => __importStar(require('sharp')));
1528
- sharp = sharpModule.default ?? sharpModule;
1529
- }
1530
- catch {
1531
- return false;
1532
- }
1533
- try {
1534
- const { data, info } = await sharp(filePath, { failOn: 'error' })
1535
- .rotate()
1536
- .resize(OWNED_RUNTIME_IMAGE_CHECK_SIZE, OWNED_RUNTIME_IMAGE_CHECK_SIZE, { fit: 'fill' })
1537
- .ensureAlpha()
1538
- .raw()
1539
- .toBuffer({ resolveWithObject: true });
1540
- const hasPreviewBackground = looksLikeBakedCheckerboardPreview({
1541
- data,
1542
- width: info.width,
1543
- height: info.height,
1544
- channels: info.channels,
1545
- }) || looksLikeBakedSolidMattePreview({
1546
- data,
1547
- width: info.width,
1548
- height: info.height,
1549
- channels: info.channels,
1550
- }) || (!options.allowNeutralSolidMatte && looksLikeBakedNeutralSolidMattePreview({
1551
- data,
1552
- width: info.width,
1553
- height: info.height,
1554
- channels: info.channels,
1555
- }));
1556
- if (hasPreviewBackground) {
1557
- return true;
1558
- }
1559
- }
1560
- catch {
1561
- return false;
1562
- }
1563
- try {
1564
- const { data, info } = await sharp(filePath, { failOn: 'error' })
1565
- .rotate()
1566
- .resize(OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, { fit: 'inside', withoutEnlargement: true })
1567
- .ensureAlpha()
1568
- .raw()
1569
- .toBuffer({ resolveWithObject: true });
1570
- if (looksLikeResidualMatteGridArtifact({
1571
- data,
1572
- width: info.width,
1573
- height: info.height,
1574
- channels: info.channels,
1575
- }) || looksLikeResidualTransparentGridLineArtifact({
1576
- data,
1577
- width: info.width,
1578
- height: info.height,
1579
- channels: info.channels,
1580
- })) {
1581
- return true;
1582
- }
1583
- }
1584
- catch {
1585
- return false;
1586
- }
1587
- return false;
1588
- }
1589
- async function assertNewGameOwnedRuntimeImageAssetsAreClean(task) {
1590
- const unsafeAssets = [];
1591
- for (const ownedAsset of task.ownedAssets) {
1592
- if (!isOwnedRuntimeImageAsset(task, ownedAsset)) {
1593
- continue;
1594
- }
1595
- const primaryPath = ownedAsset.filePaths?.primary;
1596
- if (!primaryPath) {
1597
- continue;
1598
- }
1599
- if (await imageHasBakedPreviewBackground(primaryPath, {
1600
- allowNeutralSolidMatte: isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset),
1601
- })) {
1602
- unsafeAssets.push(`${ownedAsset.name}:${ownedAsset.runtimeKey}`);
1603
- }
1604
- }
1605
- if (unsafeAssets.length > 0) {
1606
- throw new Error(`agent_task_owned_image_asset_checkerboard_background: owned runtime image assets must not contain baked transparency checkerboard, matte-color, or preview backgrounds. Recreate the asset through the PlayDrop make-assets workflow, use a cleaner PlayDrop asset, or use the image as non-runtime listing art only. Unsafe owned assets: ${unsafeAssets.join(', ')}`);
1607
- }
1608
- }
1609
1235
  const RUNTIME_SOURCE_EXTENSIONS = new Set([
1610
1236
  '.html',
1611
1237
  '.js',
@@ -2039,7 +1665,7 @@ function readPlaytestEvidenceManifest(task) {
2039
1665
  throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must be valid JSON. ${reason}`));
2040
1666
  }
2041
1667
  }
2042
- const PLAYTEST_EVIDENCE_MANIFEST_HELP = 'Expected shape: {"version":1,"entries":[{"environment":"local","url":"http://localhost:8080/...","surface":"desktop","checkedAt":"2026-07-05T00:00:00.000Z","captures":["assets/marketing/playdrop/screenshots/landscape/gameplay.png"],"actions":["start game","verify input works","reach loop progression or loss"],"statesObserved":["ready","input works","loop progressed"],"consoleErrors":[]}]} Include at least one non-final entry whose checkedAt is after the last source code change. The uploader appends the final published-route entry automatically.';
1668
+ const PLAYTEST_EVIDENCE_MANIFEST_HELP = 'Expected shape: {"version":1,"entries":[{"environment":"local","url":"http://localhost:8080/...","surface":"desktop","checkedAt":"2026-07-05T00:00:00.000Z","captures":["evidence/input.png","evidence/win.png","evidence/loss.png"],"actions":["press ArrowRight","complete the final round","allow health to reach zero"],"statesObserved":["player moved right","victory overlay","defeat overlay"],"consoleErrors":[]}],"proof":{"primaryInput":{"action":"press ArrowRight","observed":"player moved right","capture":"evidence/input.png"},"win":{"action":"complete the final round","observed":"victory overlay appeared","capture":"evidence/win.png"},"loss":{"action":"allow health to reach zero","observed":"defeat overlay appeared","capture":"evidence/loss.png"}}}. NEW_GAME proof requires three distinct captures from a current non-final entry. The uploader appends hashes and the final published-route entry automatically.';
2043
1669
  function formatMissingPlaytestEvidenceManifestError(message) {
2044
1670
  return `agent_task_playtest_manifest_missing: ${message} ${PLAYTEST_EVIDENCE_MANIFEST_HELP}`;
2045
1671
  }
@@ -2071,8 +1697,51 @@ function assertPlaytestCapturePaths(task, captures) {
2071
1697
  }
2072
1698
  }
2073
1699
  }
2074
- function assertTaskPlaytestEvidenceManifest(task) {
1700
+ function assertExactObjectKeys(record, allowedKeys, field) {
1701
+ const allowed = new Set(allowedKeys);
1702
+ const unexpected = Object.keys(record).find((key) => !allowed.has(key));
1703
+ if (unexpected) {
1704
+ throw new Error(formatPlaytestEvidenceManifestError(`${field} contains unsupported key "${unexpected}".`));
1705
+ }
1706
+ }
1707
+ function buildRequiredTaskPlaytestProof(task, manifest, currentSelfPlaytestCaptures) {
1708
+ if (!manifest.proof || typeof manifest.proof !== 'object' || Array.isArray(manifest.proof)) {
1709
+ throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json proof must demonstrate primaryInput, win, and loss for NEW_GAME tasks.'));
1710
+ }
1711
+ const rawProof = manifest.proof;
1712
+ assertExactObjectKeys(rawProof, ['primaryInput', 'win', 'loss'], 'proof');
1713
+ const proof = { version: 1 };
1714
+ for (const momentName of ['primaryInput', 'win', 'loss']) {
1715
+ const rawMoment = rawProof[momentName];
1716
+ if (!rawMoment || typeof rawMoment !== 'object' || Array.isArray(rawMoment)) {
1717
+ throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName} must include action, observed, and capture.`));
1718
+ }
1719
+ const moment = rawMoment;
1720
+ assertExactObjectKeys(moment, ['action', 'observed', 'capture'], `proof.${momentName}`);
1721
+ const action = typeof moment.action === 'string' ? moment.action.trim() : '';
1722
+ const observed = typeof moment.observed === 'string' ? moment.observed.trim() : '';
1723
+ const capture = typeof moment.capture === 'string' ? moment.capture.trim() : '';
1724
+ if (!action || !observed || !capture) {
1725
+ throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName} must include non-empty action, observed, and capture strings.`));
1726
+ }
1727
+ if (!currentSelfPlaytestCaptures.has(capture)) {
1728
+ throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName}.capture must be listed by a current non-final playtest entry.`));
1729
+ }
1730
+ const capturePath = (0, node_path_1.resolve)(task.projectDir, capture);
1731
+ const captureSha256 = (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(capturePath)).digest('hex');
1732
+ proof[momentName] = { action, observed, capture, captureSha256 };
1733
+ }
1734
+ const parsed = (0, types_1.normalizeAgentTaskPlaytestProof)(proof);
1735
+ if (!parsed.ok) {
1736
+ throw new Error(formatPlaytestEvidenceManifestError(parsed.error));
1737
+ }
1738
+ return parsed.value;
1739
+ }
1740
+ function assertTaskPlaytestEvidenceManifest(task, options = {}) {
2075
1741
  const manifest = readPlaytestEvidenceManifest(task);
1742
+ if (manifest.version !== 1) {
1743
+ throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json version must be 1.'));
1744
+ }
2076
1745
  const entries = Array.isArray(manifest.entries) ? manifest.entries : [];
2077
1746
  if (entries.length === 0) {
2078
1747
  throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json entries must include a non-final self-playtest before upload.'));
@@ -2080,6 +1749,7 @@ function assertTaskPlaytestEvidenceManifest(task) {
2080
1749
  const sourceLastModifiedAt = readRuntimeSourceLastModifiedAt(task.projectDir);
2081
1750
  const latestSourceMs = sourceLastModifiedAt ? Date.parse(sourceLastModifiedAt) : null;
2082
1751
  let hasCurrentSelfPlaytest = false;
1752
+ const currentSelfPlaytestCaptures = new Set();
2083
1753
  entries.forEach((entry, index) => {
2084
1754
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
2085
1755
  throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}] must be an object.`));
@@ -2104,11 +1774,15 @@ function assertTaskPlaytestEvidenceManifest(task) {
2104
1774
  assertPlaytestCapturePaths(task, captures);
2105
1775
  if (!isFinalGateEntry && (latestSourceMs === null || checkedAtMs >= latestSourceMs)) {
2106
1776
  hasCurrentSelfPlaytest = true;
1777
+ captures.forEach((capture) => currentSelfPlaytestCaptures.add(capture));
2107
1778
  }
2108
1779
  });
2109
1780
  if (!hasCurrentSelfPlaytest) {
2110
1781
  throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must include a non-final self-playtest checkedAt after the last source change${sourceLastModifiedAt ? ` (${sourceLastModifiedAt})` : ''}.`));
2111
1782
  }
1783
+ return options.requireOutcomeProof
1784
+ ? buildRequiredTaskPlaytestProof(task, manifest, currentSelfPlaytestCaptures)
1785
+ : null;
2112
1786
  }
2113
1787
  function readRuntimeSourceLastModifiedAt(rootDir) {
2114
1788
  const root = (0, node_path_1.resolve)(rootDir);
@@ -2185,15 +1859,14 @@ async function publishWorkerAppProject(input) {
2185
1859
  if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2186
1860
  (0, listingPreflight_1.assertAgentGameHeroListingPreflight)(task);
2187
1861
  }
1862
+ if (input.kind === 'NEW_GAME') {
1863
+ (0, listingPreflight_1.assertAgentNewGameArtDirectionPreflight)(task);
1864
+ }
2188
1865
  if (!appTaskSatisfiesPlaydropAssetRequirement(task, input.playdropAssetRequirement)) {
2189
1866
  throw new Error('agent_task_playdrop_asset_dependency_missing: the creator asked for PlayDrop assets, but catalogue.json does not declare a matching uses.assets, uses.packs, or ownedAssets dependency.');
2190
1867
  }
2191
1868
  if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2192
- await assertNewGameDirectPlaydropAssetsAreRuntimeSafe({
2193
- client: input.client,
2194
- task,
2195
- });
2196
- await assertNewGameOwnedRuntimeImageAssetsAreClean(task);
1869
+ await (0, ownedRuntimeImageValidation_1.assertOwnedRuntimeImagesAreValid)(task);
2197
1870
  }
2198
1871
  assertRequestedPlaydropAssetsAreUsedAtRuntime({
2199
1872
  task,
@@ -2231,9 +1904,12 @@ async function publishWorkerAppProject(input) {
2231
1904
  taskId: input.taskId,
2232
1905
  taskToken,
2233
1906
  };
1907
+ let playtestProof = null;
2234
1908
  if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2235
1909
  (0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(task);
2236
- assertTaskPlaytestEvidenceManifest(task);
1910
+ playtestProof = assertTaskPlaytestEvidenceManifest(task, {
1911
+ requireOutcomeProof: input.kind === 'NEW_GAME',
1912
+ });
2237
1913
  }
2238
1914
  const { upload: uploadResult, warnings } = await (0, apps_1.runAppPipeline)(input.client, task, {
2239
1915
  creatorUsername,
@@ -2245,6 +1921,7 @@ async function publishWorkerAppProject(input) {
2245
1921
  runStagedUploadLoadCheck: true,
2246
1922
  ensureRegisteredAppShell: true,
2247
1923
  agentTaskId: input.taskId,
1924
+ playtestProof: playtestProof ?? undefined,
2248
1925
  mediaCaptureRequired: input.executionTarget === 'FIRST_PARTY',
2249
1926
  captureSession,
2250
1927
  });
@@ -310,6 +310,22 @@ export declare function acquireWorkerSupervisorLock(input: {
310
310
  file?: string;
311
311
  }): WorkerSupervisorLock;
312
312
  export declare function releaseWorkerSupervisorLock(lock: WorkerSupervisorLock): void;
313
+ export type TaskUploadLockOwner = {
314
+ schemaVersion: 1;
315
+ command: 'task upload';
316
+ pid: number;
317
+ processStartIdentity: string;
318
+ createdAt: string;
319
+ };
320
+ export type TaskUploadLock = {
321
+ file: string;
322
+ owner: TaskUploadLockOwner;
323
+ };
324
+ export declare function resolveTaskUploadLockFile(workspaceDir: string): string;
325
+ export declare function removeStaleTaskUploadLockIfUnchanged(file: string, observedHolder: TaskUploadLockOwner): boolean;
326
+ export declare function acquireTaskUploadLock(workspaceDir: string): TaskUploadLock;
327
+ export declare function releaseTaskUploadLock(lock: TaskUploadLock): void;
328
+ export declare function withTaskUploadLock<T>(workspaceDir: string, work: () => Promise<T>): Promise<T>;
313
329
  export declare function readActivePersonalWorkerRuntimeState(): WorkerRuntimeState | null;
314
330
  export declare function parseLaunchAgentProgramArguments(plist: string): string[];
315
331
  export declare function assertLaunchAgentPlistCompatible(input: {