@playdrop/playdrop-cli 0.10.21 → 0.11.4

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.
@@ -50,6 +50,8 @@ const taskUtils_1 = require("../taskUtils");
50
50
  const uploadLog_1 = require("../uploadLog");
51
51
  const upload_content_1 = require("./upload-content");
52
52
  const appUrls_1 = require("../appUrls");
53
+ const launchCheck_1 = require("../apps/launchCheck");
54
+ const devRuntimeAssets_1 = require("./devRuntimeAssets");
53
55
  const upload_graph_1 = require("./upload-graph");
54
56
  function normalizeSavedItemKinds(kinds) {
55
57
  return [...new Set(Array.isArray(kinds) ? kinds : [])].sort();
@@ -1250,9 +1252,6 @@ function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
1250
1252
  });
1251
1253
  return hasFirstPartyPack || hasFirstPartyAsset || task.ownedAssets.length > 0;
1252
1254
  }
1253
- function appTaskHasRequiredNewGameAssets(task) {
1254
- return task.uses.packs.length > 0 || task.uses.assets.length > 0 || task.ownedAssets.length > 0;
1255
- }
1256
1255
  function isUnsafeDirectPlaydropAiImageAsset(detail, parsedRevision) {
1257
1256
  const asset = detail?.asset;
1258
1257
  if (!asset || asset.category !== 'IMAGE') {
@@ -1285,7 +1284,7 @@ async function assertNewGameDirectPlaydropAssetsAreRuntimeSafe(input) {
1285
1284
  }
1286
1285
  }
1287
1286
  if (unsafeRefs.length > 0) {
1288
- 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 owned PNG assets through the PlayDrop plugin asset-extraction-2d workflow instead. Unsafe refs: ${unsafeRefs.join(', ')}`);
1287
+ 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(', ')}`);
1289
1288
  }
1290
1289
  }
1291
1290
  function isOwnedRuntimeImageAsset(task, ownedAsset) {
@@ -1605,7 +1604,7 @@ async function assertNewGameOwnedRuntimeImageAssetsAreClean(task) {
1605
1604
  }
1606
1605
  }
1607
1606
  if (unsafeAssets.length > 0) {
1608
- 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 plugin asset-extraction-2d workflow, use a cleaner PlayDrop asset, or use the image as non-runtime listing art only. Unsafe owned assets: ${unsafeAssets.join(', ')}`);
1607
+ 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(', ')}`);
1609
1608
  }
1610
1609
  }
1611
1610
  const RUNTIME_SOURCE_EXTENSIONS = new Set([
@@ -1673,6 +1672,95 @@ function hasDisallowedGameplayAssetFallback(source) {
1673
1672
  const executableSource = stripRuntimeSourceCommentsAndStrings(source);
1674
1673
  return /\bdrawFallback[A-Za-z0-9_]*\b|\bfallback(?:Image|Sprite|Asset|Draw|Visual|Character|Obstacle|Collectible|Prop|Enemy|Item|Piñata|Pinata|Candy)?\b|\bplaceholder(?:Image|Sprite|Asset|Draw|Visual|Character|Obstacle|Collectible|Prop|Enemy|Item)?\b|img\.onerror\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*(?:\{[^}]*\bresolve\s*\(|\bresolve\s*\(|\{[^}]*\breturn\b)|if\s*\([^)]*!url[^)]*\)\s*\{[^}]*\bresolve\s*\([^}]*\breturn\b|if\s*\([^)]*!url[^)]*\)\s*(?:return|continue|resolve\s*\()/i.test(executableSource);
1675
1674
  }
1675
+ function formatRuntimeSourceLocation(runtimeSource, offset) {
1676
+ const location = resolveRuntimeSourceLocation(runtimeSource, offset);
1677
+ if (!location) {
1678
+ return 'unknown source location';
1679
+ }
1680
+ return `${location.path}:${location.line}:${location.column}`;
1681
+ }
1682
+ function findRuntimeSourcePattern(runtimeSource, pattern) {
1683
+ const match = pattern.exec(runtimeSource.source);
1684
+ if (!match) {
1685
+ return null;
1686
+ }
1687
+ return {
1688
+ match: match[0],
1689
+ location: formatRuntimeSourceLocation(runtimeSource, match.index),
1690
+ };
1691
+ }
1692
+ function assertNoLaunchCheckConditionalSource(runtimeSource) {
1693
+ const directMarker = findRuntimeSourcePattern(runtimeSource, /\b(?:launchCheck|launch-check|upload-check|previewToken|localDevPort)\b/i);
1694
+ if (directMarker) {
1695
+ throw new Error(`agent_task_launch_check_conditional_disallowed: game source must not branch on PlayDrop validation markers. First match "${directMarker.match}" at ${directMarker.location}.`);
1696
+ }
1697
+ const environmentSniff = findRuntimeSourcePattern(runtimeSource, /(?:document\.referrer|window\.location|\blocation\.(?:href|search|origin)|new\s+URLSearchParams)[\s\S]{0,320}(?:localhost|127\.0\.0\.1|_auth\/creators|\/upload-check\/)|(?:localhost|127\.0\.0\.1|_auth\/creators|\/upload-check\/)[\s\S]{0,320}(?:document\.referrer|window\.location|\blocation\.(?:href|search|origin)|new\s+URLSearchParams)/i);
1698
+ if (environmentSniff) {
1699
+ throw new Error(`agent_task_launch_check_conditional_disallowed: game source must not sniff validation or local route context. First match at ${environmentSniff.location}.`);
1700
+ }
1701
+ }
1702
+ function hasProceduralAssetStrategy(task) {
1703
+ const strategy = task.design?.assetStrategy;
1704
+ return strategy?.value === 'procedural';
1705
+ }
1706
+ function hasDeclaredRuntimeAssetDependency(task) {
1707
+ return task.uses.packs.length > 0
1708
+ || task.uses.assets.length > 0
1709
+ || task.ownedAssets.some((ownedAsset) => !isListingOwnedAsset(task, ownedAsset));
1710
+ }
1711
+ function assertNewGameAssetStrategy(task) {
1712
+ if (hasDeclaredRuntimeAssetDependency(task)) {
1713
+ return;
1714
+ }
1715
+ if (hasProceduralAssetStrategy(task)) {
1716
+ return;
1717
+ }
1718
+ throw new Error('agent_task_asset_strategy_missing: new game tasks must either declare true runtime assets/packs or set design.assetStrategy to { "value": "procedural", "note": "..." } for an honest procedural draft.');
1719
+ }
1720
+ function isListingOwnedAsset(task, ownedAsset) {
1721
+ const primaryPath = ownedAsset.filePaths?.primary;
1722
+ if (!primaryPath) {
1723
+ return false;
1724
+ }
1725
+ const relativePath = primaryPath.slice((0, node_path_1.resolve)(task.projectDir).length + 1).replace(/\\/g, '/');
1726
+ return relativePath.startsWith('assets/marketing/') || relativePath.startsWith('listing/');
1727
+ }
1728
+ function isAudioOwnedAsset(ownedAsset) {
1729
+ return ownedAsset.category === 'AUDIO';
1730
+ }
1731
+ function sourceMentionsRuntimeKey(source, runtimeKey) {
1732
+ return source.includes(runtimeKey);
1733
+ }
1734
+ function hasRuntimeAssetLoadOrRenderPath(source) {
1735
+ return /\bsdk\.assets\b|\.resolveAppAsset\s*\(|\.listAppAssets\s*\(/.test(source)
1736
+ && /\bnew\s+(?:Image|Audio)\s*\(|createImageBitmap\s*\(|\.src\s*=|\.load(?:Async)?\s*\(|\.drawImage\s*\(|\.add\.image\s*\(|\.load\.image\s*\(|\.textures\.add|\.sound\.add|\.scene\.add\s*\(/i.test(source);
1737
+ }
1738
+ function assertAudioAssetsAreNonBlocking(task, runtimeSource) {
1739
+ const source = runtimeSource.source;
1740
+ for (const ownedAsset of task.ownedAssets) {
1741
+ if (!isAudioOwnedAsset(ownedAsset)) {
1742
+ continue;
1743
+ }
1744
+ const runtimeKey = ownedAsset.runtimeKey?.trim() ?? '';
1745
+ const name = ownedAsset.name.trim();
1746
+ if (!runtimeKey || (!source.includes(runtimeKey) && !source.includes(name))) {
1747
+ continue;
1748
+ }
1749
+ const lowerRuntimeKey = runtimeKey.toLowerCase();
1750
+ const lowerName = name.toLowerCase();
1751
+ const executableSource = stripRuntimeSourceCommentsAndStrings(source);
1752
+ const fatalAudioPattern = /\bthrow\s+new\s+Error\s*\([^)]*(?:missing|asset|sound|audio|sfx)[^)]*\)|\bthrow\s+new\s+Error\s*\([^)]*\)|Promise\.reject\s*\([^)]*(?:missing|asset|sound|audio|sfx)[^)]*\)/ig;
1753
+ let match;
1754
+ while ((match = fatalAudioPattern.exec(executableSource)) !== null) {
1755
+ const start = Math.max(0, match.index - 800);
1756
+ const end = Math.min(source.length, match.index + 800);
1757
+ const nearby = source.slice(start, end).toLowerCase();
1758
+ if (nearby.includes(lowerRuntimeKey) || nearby.includes(lowerName)) {
1759
+ throw new Error(`agent_task_audio_asset_boot_fatal_disallowed: audio SFX owned asset "${ownedAsset.name}" must not block play when it fails to load; log a console warning and continue.`);
1760
+ }
1761
+ }
1762
+ }
1763
+ }
1676
1764
  function hasRuntimeAssetUrlExtensionAssumption(source) {
1677
1765
  return /(?:\.url|\.href|\burl\b)[^;\n]{0,220}(?:\\\.gl(?:b|tf)|\.gl(?:b|tf)|\.endsWith\s*\(\s*['"]\.gl(?:b|tf)['"]\s*\)|\.includes\s*\(\s*['"]\.gl(?:b|tf)['"]\s*\)|\.match\s*\([^)]*\.gl(?:b|tf))/i.test(source);
1678
1766
  }
@@ -1873,15 +1961,64 @@ function assertRequestedPlaydropAssetsAreUsedAtRuntime(input) {
1873
1961
  }
1874
1962
  }
1875
1963
  }
1876
- function assertNewGameDeclaredAssetsAreUsedAtRuntime(task) {
1877
- const source = readRuntimeSourceText(task.projectDir).source;
1964
+ async function assertNewGameDeclaredAssetsAreUsedAtRuntime(input) {
1965
+ const runtimeSource = readRuntimeSourceText(input.task.projectDir);
1966
+ const source = runtimeSource.source;
1967
+ assertNoLaunchCheckConditionalSource(runtimeSource);
1968
+ assertNewGameAssetStrategy(input.task);
1969
+ if (!hasDeclaredRuntimeAssetDependency(input.task)) {
1970
+ return;
1971
+ }
1878
1972
  if (!/\bsdk\.assets\b|\.resolveAppAsset\s*\(|\.listAppAssets\s*\(/.test(source)) {
1879
1973
  throw new Error('agent_task_game_assets_runtime_missing: new game tasks must load declared gameplay assets through sdk.assets at runtime.');
1880
1974
  }
1881
- if (task.uses.packs.length > 0 && !/\.listAppAssets\s*\(/.test(source)) {
1975
+ if (input.task.uses.packs.length > 0 && !/\.listAppAssets\s*\(/.test(source)) {
1882
1976
  throw new Error('agent_task_game_pack_runtime_missing: new game tasks that declare uses.packs must enumerate pack assets with sdk.assets.listAppAssets().');
1883
1977
  }
1884
- const hasDirectGameplayAssets = task.ownedAssets.length > 0 || task.uses.assets.length > 0;
1978
+ if (!hasRuntimeAssetLoadOrRenderPath(source)) {
1979
+ throw new Error('agent_task_game_assets_runtime_missing: declared game assets must be loaded and rendered or played at runtime, not only listed in catalogue.json.');
1980
+ }
1981
+ if (input.task.uses.packs.length > 0 && !/\bassetRef\b|\bsourceType\b|\bcontentType\b/i.test(source)) {
1982
+ throw new Error('agent_task_game_pack_runtime_missing: new game tasks that declare uses.packs must select pack assets from the runtime manifest by assetRef/sourceType/contentType before rendering them.');
1983
+ }
1984
+ const manifest = input.task.ownedAssets.length > 0
1985
+ ? await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
1986
+ client: input.client,
1987
+ apiBase: input.apiBase,
1988
+ task: {
1989
+ ...input.task,
1990
+ uses: { assets: [], packs: [] },
1991
+ },
1992
+ creatorUsername: input.creatorUsername,
1993
+ appBaseUrl: `http://127.0.0.1/apps/dev/${encodeURIComponent(input.creatorUsername)}/${encodeURIComponent((0, appUrls_1.getAppTypeSlug)(input.task.type ?? 'GAME'))}/${encodeURIComponent(input.task.name)}/`,
1994
+ })
1995
+ : null;
1996
+ const runtimeKeys = new Set((manifest?.response.assets ?? [])
1997
+ .map((asset) => typeof asset.runtimeKey === 'string' ? asset.runtimeKey.trim() : '')
1998
+ .filter(Boolean));
1999
+ for (const ownedAsset of input.task.ownedAssets) {
2000
+ const runtimeKey = ownedAsset.runtimeKey?.trim() ?? '';
2001
+ if (!runtimeKey) {
2002
+ throw new Error(`agent_task_owned_asset_runtime_key_missing: owned asset "${ownedAsset.name}" must declare a runtimeKey.`);
2003
+ }
2004
+ if (!runtimeKeys.has(runtimeKey)) {
2005
+ throw new Error(`agent_task_owned_asset_runtime_manifest_missing: owned asset "${ownedAsset.name}" runtimeKey "${runtimeKey}" was not present in the runtime manifest.`);
2006
+ }
2007
+ if (isListingOwnedAsset(input.task, ownedAsset)) {
2008
+ continue;
2009
+ }
2010
+ if (!sourceMentionsRuntimeKey(source, runtimeKey)) {
2011
+ throw new Error(`agent_task_owned_asset_runtime_key_unused: owned asset "${ownedAsset.name}" runtimeKey "${runtimeKey}" does not appear in runtime source.`);
2012
+ }
2013
+ }
2014
+ for (const dependency of input.task.uses.assets) {
2015
+ const runtimeKey = dependency.runtimeKey?.trim() ?? '';
2016
+ if (runtimeKey && !sourceMentionsRuntimeKey(source, runtimeKey)) {
2017
+ throw new Error(`agent_task_declared_asset_runtime_key_unused: declared asset "${dependency.ref}" runtimeKey "${runtimeKey}" does not appear in runtime source.`);
2018
+ }
2019
+ }
2020
+ assertAudioAssetsAreNonBlocking(input.task, runtimeSource);
2021
+ const hasDirectGameplayAssets = input.task.ownedAssets.some((ownedAsset) => !isListingOwnedAsset(input.task, ownedAsset)) || input.task.uses.assets.length > 0;
1885
2022
  if (hasDirectGameplayAssets && hasDisallowedGameplayAssetFallback(source)) {
1886
2023
  throw new Error('agent_task_game_asset_fallback_disallowed: new game tasks must fail clearly when declared gameplay assets cannot load instead of rendering placeholder or fallback visuals.');
1887
2024
  }
@@ -1909,79 +2046,6 @@ function readPngDimensions(filePath) {
1909
2046
  }
1910
2047
  return { width, height };
1911
2048
  }
1912
- function readPngTextMetadata(filePath) {
1913
- const buffer = (0, node_fs_1.readFileSync)(filePath);
1914
- const metadata = {};
1915
- if (buffer.length < 24) {
1916
- return metadata;
1917
- }
1918
- let offset = 8;
1919
- while (offset + 12 <= buffer.length) {
1920
- const length = buffer.readUInt32BE(offset);
1921
- const dataStart = offset + 8;
1922
- const dataEnd = dataStart + length;
1923
- const chunkEnd = dataEnd + 4;
1924
- if (chunkEnd > buffer.length) {
1925
- break;
1926
- }
1927
- const type = buffer.toString('ascii', offset + 4, offset + 8);
1928
- if (type === 'tEXt') {
1929
- const chunkData = buffer.subarray(dataStart, dataEnd);
1930
- const separatorOffset = chunkData.indexOf(0);
1931
- if (separatorOffset > 0) {
1932
- const keyword = chunkData.toString('latin1', 0, separatorOffset).trim();
1933
- const value = chunkData.toString('latin1', separatorOffset + 1).trim();
1934
- if (keyword && value) {
1935
- metadata[keyword] = value;
1936
- }
1937
- }
1938
- }
1939
- offset = chunkEnd;
1940
- if (type === 'IEND') {
1941
- break;
1942
- }
1943
- }
1944
- return metadata;
1945
- }
1946
- function normalizeHeroPromptText(value) {
1947
- return value.normalize('NFKC').replace(/\s+/g, ' ').trim().toLowerCase();
1948
- }
1949
- function heroPromptContainsRequiredTitleInstruction(prompt, displayName) {
1950
- const normalizedPrompt = normalizeHeroPromptText(prompt);
1951
- const normalizedDisplayName = normalizeHeroPromptText(displayName);
1952
- if (!normalizedDisplayName || !normalizedPrompt.includes(normalizedDisplayName)) {
1953
- return false;
1954
- }
1955
- const hasTitleLanguage = /\btitle\b|\blogo\b|\bwordmark\b/.test(normalizedPrompt);
1956
- const hasProminenceLanguage = /\bfront[-\s]?and[-\s]?center\b|\bfront center\b|\bcenter(?:ed)?\b|\bprominent\b|\blarge readable\b/.test(normalizedPrompt);
1957
- return hasTitleLanguage && hasProminenceLanguage && heroPromptForbidsExtraReadableText(prompt);
1958
- }
1959
- function heroPromptForbidsExtraReadableText(prompt) {
1960
- const normalizedPrompt = normalizeHeroPromptText(prompt);
1961
- return /\bno\b[^.]{0,180}\b(?:extra|other|additional)\b[^.]{0,180}\b(?:readable\s+)?(?:text|words|letters|numbers|banners|signs|captions|labels|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
1962
- || /\bno\b[^.]{0,180}\b(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
1963
- || /\bno\b[^.]{0,180}\b(?:readable\s+)?(?:text|words|letters|numbers|banners|signs|captions|labels|dates|taglines|subtitles)\b[^.]{0,180}\banywhere\b/i.test(normalizedPrompt)
1964
- || /\bforbid\b[^.]{0,180}\b(?:all\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
1965
- || /\b(?:text|word|letter|typography|writing)[-\s]?free\b/i.test(normalizedPrompt)
1966
- || /\bwithout\b[^.]{0,180}\b(?:any\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
1967
- || /\b(?:do\s+not|don't|dont|never)\b[^.]{0,180}\b(?:include|add|render|show|place|draw|write)\b[^.]{0,180}\b(?:any\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt)
1968
- || /\b(?:avoid|exclude|omit|remove)\b[^.]{0,180}\b(?:all\s+|any\s+)?(?:readable\s+)?(?:text|words|letters|numbers|typography|writing|banners|signs|signage|captions|labels|logos|titles|dates|taglines|subtitles)\b/i.test(normalizedPrompt);
1969
- }
1970
- function assertNewGameHeroListingPromptMetadata(input) {
1971
- const metadata = readPngTextMetadata(input.filePath);
1972
- const tool = metadata['playdrop:cliOutputTool'] ?? '';
1973
- const prompt = metadata['playdrop:cliOutputPrompt'] ?? '';
1974
- const titleTool = metadata['playdrop:listingTitleTool'] ?? '';
1975
- if (titleTool === 'playdrop compose-listing-title') {
1976
- throw new Error(`agent_task_listing_hero_art_composed_title_disallowed: ${input.field} must be generated directly by PlayDrop AI with the exact display name "${input.displayName}" integrated front and center, not added afterward by a title overlay script.`);
1977
- }
1978
- if (tool !== 'playdrop ai create image --output' || !prompt) {
1979
- throw new Error(`agent_task_listing_hero_art_metadata_missing: ${input.field} must be produced by "playdrop ai create image --output" or by the staged listing title composition script so PlayDrop can verify listing-art metadata.`);
1980
- }
1981
- if (!heroPromptContainsRequiredTitleInstruction(prompt, input.displayName)) {
1982
- throw new Error(`agent_task_listing_hero_art_title_prompt_missing: ${input.field} must be generated from a prompt containing the exact display name "${input.displayName}" as prominent front-and-center title/logo text and forbidding all extra readable text.`);
1983
- }
1984
- }
1985
2049
  function readAgentTaskCatalogueApp(task) {
1986
2050
  let data;
1987
2051
  try {
@@ -2020,10 +2084,6 @@ function assertNewGameHeroListingPath(task, filePath, field) {
2020
2084
  }
2021
2085
  }
2022
2086
  function assertNewGameHeroListingArt(task) {
2023
- const app = readAgentTaskCatalogueApp(task);
2024
- const displayName = typeof app.displayName === 'string' && app.displayName.trim()
2025
- ? app.displayName.trim()
2026
- : task.name;
2027
2087
  const heroPortraitPath = task.listing?.heroPortraitPath;
2028
2088
  const heroLandscapePath = task.listing?.heroLandscapePath;
2029
2089
  if (!heroPortraitPath || !heroLandscapePath) {
@@ -2040,18 +2100,173 @@ function assertNewGameHeroListingArt(task) {
2040
2100
  if (landscape.width < MIN_HERO_LANDSCAPE_WIDTH || landscape.height < MIN_HERO_LANDSCAPE_HEIGHT || landscape.width <= landscape.height) {
2041
2101
  throw new Error(`agent_task_listing_hero_art_invalid: listing.heroLandscape must be a landscape PNG at least ${MIN_HERO_LANDSCAPE_WIDTH}x${MIN_HERO_LANDSCAPE_HEIGHT}.`);
2042
2102
  }
2043
- assertNewGameHeroListingPromptMetadata({
2044
- projectDir: task.projectDir,
2045
- filePath: heroPortraitPath,
2046
- field: 'listing.heroPortrait',
2047
- displayName,
2048
- });
2049
- assertNewGameHeroListingPromptMetadata({
2050
- projectDir: task.projectDir,
2051
- filePath: heroLandscapePath,
2052
- field: 'listing.heroLandscape',
2053
- displayName,
2103
+ }
2104
+ function assertListingScreenshotPath(task, filePath, field) {
2105
+ const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), (0, node_path_1.resolve)(filePath)).replace(/\\/g, '/');
2106
+ const requiredPrefix = field === 'listing.screenshotsPortrait'
2107
+ ? 'assets/marketing/playdrop/screenshots/portrait/'
2108
+ : 'assets/marketing/playdrop/screenshots/landscape/';
2109
+ if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
2110
+ throw new Error(`agent_task_listing_screenshots_path_invalid: ${field} entries must point to PNG files inside the project under ${requiredPrefix}.`);
2111
+ }
2112
+ if (!relativePath.startsWith(requiredPrefix) || (0, node_path_1.extname)(relativePath).toLowerCase() !== '.png') {
2113
+ throw new Error(`agent_task_listing_screenshots_path_invalid: ${field} entries must point to PNG files under ${requiredPrefix}.`);
2114
+ }
2115
+ }
2116
+ function assertNewGameListingScreenshots(task) {
2117
+ const listing = task.listing;
2118
+ const portraitPaths = listing?.screenshotPortraitPaths ?? [];
2119
+ const landscapePaths = listing?.screenshotLandscapePaths ?? [];
2120
+ if (portraitPaths.length === 0 && landscapePaths.length === 0) {
2121
+ throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG captures.');
2122
+ }
2123
+ if (task.surfaceTargets.includes('MOBILE_PORTRAIT') && portraitPaths.length === 0) {
2124
+ throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG capture.');
2125
+ }
2126
+ if ((task.surfaceTargets.includes('DESKTOP') || task.surfaceTargets.includes('MOBILE_LANDSCAPE')) && landscapePaths.length === 0) {
2127
+ throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG capture.');
2128
+ }
2129
+ for (const filePath of portraitPaths) {
2130
+ assertListingScreenshotPath(task, filePath, 'listing.screenshotsPortrait');
2131
+ readPngDimensions(filePath);
2132
+ }
2133
+ for (const filePath of landscapePaths) {
2134
+ assertListingScreenshotPath(task, filePath, 'listing.screenshotsLandscape');
2135
+ readPngDimensions(filePath);
2136
+ }
2137
+ }
2138
+ function readPlaytestEvidenceManifest(task) {
2139
+ const manifestPath = (0, node_path_1.join)(task.projectDir, 'playtest-evidence.json');
2140
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
2141
+ throw new Error(formatMissingPlaytestEvidenceManifestError('new game tasks must include playtest-evidence.json before upload.'));
2142
+ }
2143
+ try {
2144
+ const value = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
2145
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
2146
+ throw new Error('manifest must be a JSON object');
2147
+ }
2148
+ return value;
2149
+ }
2150
+ catch (error) {
2151
+ const reason = error instanceof Error ? error.message : String(error);
2152
+ throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must be valid JSON. ${reason}`));
2153
+ }
2154
+ }
2155
+ 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.';
2156
+ function formatMissingPlaytestEvidenceManifestError(message) {
2157
+ return `agent_task_playtest_manifest_missing: ${message} ${PLAYTEST_EVIDENCE_MANIFEST_HELP}`;
2158
+ }
2159
+ function formatPlaytestEvidenceManifestError(message) {
2160
+ return `agent_task_playtest_manifest_invalid: ${message} ${PLAYTEST_EVIDENCE_MANIFEST_HELP}`;
2161
+ }
2162
+ function assertStringArrayField(value, field, options) {
2163
+ if (!Array.isArray(value)) {
2164
+ throw new Error(formatPlaytestEvidenceManifestError(`${field} must be an array.`));
2165
+ }
2166
+ const strings = value.map((entry) => typeof entry === 'string' ? entry.trim() : '');
2167
+ if (strings.some((entry) => entry.length === 0)) {
2168
+ throw new Error(formatPlaytestEvidenceManifestError(`${field} entries must be non-empty strings.`));
2169
+ }
2170
+ if (!options?.allowEmpty && strings.length === 0) {
2171
+ throw new Error(formatPlaytestEvidenceManifestError(`${field} must include at least one entry.`));
2172
+ }
2173
+ return strings;
2174
+ }
2175
+ function assertPlaytestCapturePaths(task, captures) {
2176
+ for (const capture of captures) {
2177
+ const capturePath = (0, node_path_1.resolve)(task.projectDir, capture);
2178
+ const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), capturePath).replace(/\\/g, '/');
2179
+ if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
2180
+ throw new Error(formatPlaytestEvidenceManifestError('capture paths must stay inside the project.'));
2181
+ }
2182
+ if (!(0, node_fs_1.existsSync)(capturePath) || !(0, node_fs_1.statSync)(capturePath).isFile()) {
2183
+ throw new Error(formatPlaytestEvidenceManifestError(`capture file "${capture}" does not exist.`));
2184
+ }
2185
+ }
2186
+ }
2187
+ function assertNewGamePlaytestEvidenceManifest(task) {
2188
+ const manifest = readPlaytestEvidenceManifest(task);
2189
+ const entries = Array.isArray(manifest.entries) ? manifest.entries : [];
2190
+ if (entries.length === 0) {
2191
+ throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json entries must include a non-final self-playtest before upload.'));
2192
+ }
2193
+ const sourceLastModifiedAt = readRuntimeSourceLastModifiedAt(task.projectDir);
2194
+ const latestSourceMs = sourceLastModifiedAt ? Date.parse(sourceLastModifiedAt) : null;
2195
+ let hasCurrentSelfPlaytest = false;
2196
+ entries.forEach((entry, index) => {
2197
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
2198
+ throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}] must be an object.`));
2199
+ }
2200
+ const record = entry;
2201
+ const environment = typeof record.environment === 'string' ? record.environment.trim() : '';
2202
+ const url = typeof record.url === 'string' ? record.url.trim() : '';
2203
+ const surface = typeof record.surface === 'string' ? record.surface.trim() : '';
2204
+ const checkedAt = typeof record.checkedAt === 'string' ? record.checkedAt.trim() : '';
2205
+ if (!environment || !url || !surface || !checkedAt) {
2206
+ throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}] must include environment, url, surface, and checkedAt.`));
2207
+ }
2208
+ const checkedAtMs = Date.parse(checkedAt);
2209
+ if (!Number.isFinite(checkedAtMs)) {
2210
+ throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}].checkedAt must be an ISO date string.`));
2211
+ }
2212
+ const isFinalGateEntry = environment === 'final';
2213
+ const captures = assertStringArrayField(record.captures, `entries[${index}].captures`, { allowEmpty: isFinalGateEntry });
2214
+ assertStringArrayField(record.actions, `entries[${index}].actions`);
2215
+ assertStringArrayField(record.statesObserved, `entries[${index}].statesObserved`, { allowEmpty: isFinalGateEntry });
2216
+ assertStringArrayField(record.consoleErrors, `entries[${index}].consoleErrors`, { allowEmpty: true });
2217
+ assertPlaytestCapturePaths(task, captures);
2218
+ if (!isFinalGateEntry && (latestSourceMs === null || checkedAtMs >= latestSourceMs)) {
2219
+ hasCurrentSelfPlaytest = true;
2220
+ }
2054
2221
  });
2222
+ if (!hasCurrentSelfPlaytest) {
2223
+ throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must include a non-final self-playtest checkedAt after the last source change${sourceLastModifiedAt ? ` (${sourceLastModifiedAt})` : ''}.`));
2224
+ }
2225
+ }
2226
+ function writeFinalPublishedRouteEvidence(input) {
2227
+ const manifest = readPlaytestEvidenceManifest(input.task);
2228
+ const entries = Array.isArray(manifest.entries) ? manifest.entries : [];
2229
+ const finalEntry = {
2230
+ environment: 'final',
2231
+ url: input.url,
2232
+ surface: 'DESKTOP',
2233
+ captures: [],
2234
+ actions: ['open owner draft play URL'],
2235
+ statesObserved: input.status === 'PASSED' ? ['ready', 'nonblank-first-frame'] : [],
2236
+ consoleErrors: input.message ? [input.message] : [],
2237
+ checkedAt: input.checkedAt,
2238
+ sourceLastModifiedAt: readRuntimeSourceLastModifiedAt(input.task.projectDir),
2239
+ };
2240
+ const nextManifest = {
2241
+ ...manifest,
2242
+ version: 1,
2243
+ entries: [...entries, finalEntry],
2244
+ finalPublishedRoute: finalEntry,
2245
+ };
2246
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(input.task.projectDir, 'playtest-evidence.json'), `${JSON.stringify(nextManifest, null, 2)}\n`, 'utf8');
2247
+ }
2248
+ function readRuntimeSourceLastModifiedAt(rootDir) {
2249
+ const root = (0, node_path_1.resolve)(rootDir);
2250
+ let latest = 0;
2251
+ const walk = (dir) => {
2252
+ for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
2253
+ const absolute = (0, node_path_1.join)(dir, entry.name);
2254
+ const relativePath = absolute.slice(root.length + 1).replace(/\\/g, '/');
2255
+ if (entry.isDirectory()) {
2256
+ if (RUNTIME_SOURCE_EXCLUDED_DIRS.has(entry.name) || RUNTIME_SOURCE_EXCLUDED_DIRS.has(relativePath)) {
2257
+ continue;
2258
+ }
2259
+ walk(absolute);
2260
+ continue;
2261
+ }
2262
+ if (!entry.isFile() || !RUNTIME_SOURCE_EXTENSIONS.has((0, node_path_1.extname)(entry.name).toLowerCase())) {
2263
+ continue;
2264
+ }
2265
+ latest = Math.max(latest, (0, node_fs_1.statSync)(absolute).mtimeMs);
2266
+ }
2267
+ };
2268
+ walk(root);
2269
+ return latest > 0 ? new Date(latest).toISOString() : null;
2055
2270
  }
2056
2271
  // Task upload path used by "playdrop task upload": same internal pipeline as
2057
2272
  // "playdrop project publish", but always as one PRIVATE draft version for the
@@ -2101,9 +2316,6 @@ async function publishWorkerAppProject(input) {
2101
2316
  }
2102
2317
  if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2103
2318
  assertNewGameHeroListingArt(task);
2104
- if (!appTaskHasRequiredNewGameAssets(task)) {
2105
- throw new Error('agent_task_game_assets_missing: new game tasks must declare at least one real owned asset, PlayDrop asset, or PlayDrop asset pack in catalogue.json.');
2106
- }
2107
2319
  }
2108
2320
  if (!appTaskSatisfiesPlaydropAssetRequirement(task, input.playdropAssetRequirement)) {
2109
2321
  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.');
@@ -2114,13 +2326,20 @@ async function publishWorkerAppProject(input) {
2114
2326
  task,
2115
2327
  });
2116
2328
  await assertNewGameOwnedRuntimeImageAssetsAreClean(task);
2117
- assertNewGameDeclaredAssetsAreUsedAtRuntime(task);
2118
2329
  }
2119
2330
  assertRequestedPlaydropAssetsAreUsedAtRuntime({
2120
2331
  task,
2121
2332
  requirement: input.playdropAssetRequirement,
2122
2333
  creatorRequest: input.creatorRequest,
2123
2334
  });
2335
+ if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2336
+ await assertNewGameDeclaredAssetsAreUsedAtRuntime({
2337
+ client: input.client,
2338
+ apiBase: input.apiBase,
2339
+ creatorUsername,
2340
+ task,
2341
+ });
2342
+ }
2124
2343
  await assertDeclared3dPlaydropPacksContainRuntimeModels({
2125
2344
  client: input.client,
2126
2345
  task,
@@ -2135,6 +2354,10 @@ async function publishWorkerAppProject(input) {
2135
2354
  if (typeof app.id !== 'number' || !Number.isInteger(app.id) || app.id <= 0) {
2136
2355
  throw new Error(`App "${task.name}" registration did not return an app id.`);
2137
2356
  }
2357
+ if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2358
+ assertNewGameListingScreenshots(task);
2359
+ assertNewGamePlaytestEvidenceManifest(task);
2360
+ }
2138
2361
  const { upload: uploadResult, warnings } = await (0, apps_1.runAppPipeline)(input.client, task, {
2139
2362
  creatorUsername,
2140
2363
  apiBase: input.apiBase,
@@ -2155,6 +2378,28 @@ async function publishWorkerAppProject(input) {
2155
2378
  if (typeof uploadResult.versionNodeId !== 'string' || uploadResult.versionNodeId.trim().length === 0) {
2156
2379
  throw new Error(`App "${task.name}" upload did not return versionNodeId.`);
2157
2380
  }
2381
+ const finalPlayUrl = (0, appUrls_1.buildPlatformPrivateDraftPlayUrl)(input.webBase ?? null, {
2382
+ creatorUsername,
2383
+ appName: task.name,
2384
+ appType: task.type ?? 'GAME',
2385
+ version: uploadResult.version,
2386
+ });
2387
+ const finalPlayCheck = await (0, launchCheck_1.runPublishedHostedPlayCheck)({
2388
+ targetUrl: finalPlayUrl,
2389
+ surfaceTargets: task.surfaceTargets,
2390
+ token: input.token,
2391
+ currentUser: input.user,
2392
+ });
2393
+ writeFinalPublishedRouteEvidence({
2394
+ task,
2395
+ url: finalPlayUrl,
2396
+ checkedAt: finalPlayCheck.checkedAt,
2397
+ status: finalPlayCheck.status,
2398
+ message: finalPlayCheck.message,
2399
+ });
2400
+ if (finalPlayCheck.status !== 'PASSED') {
2401
+ throw new Error((0, launchCheck_1.formatHostedLaunchCheckFailure)(task.name, finalPlayCheck, 'final'));
2402
+ }
2158
2403
  return {
2159
2404
  appId: app.id,
2160
2405
  appVersionId: uploadResult.versionId,
@@ -5,7 +5,7 @@ import { type LoggedProcessResult, type LoggedProcessTranscriptChunk } from './w
5
5
  export { DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, assertWorkerTokenUsageWithinCap, buildCodexExecArgs, buildClaudeExecArgs, buildClaudePermissionSettings, buildWorkerChildEnv, extractAgentTokenUsage, extractCodexRolloutTokenUsage, extractCodexThreadId, extractCodexTokensUsed, readEnvBoolean, readCodexSandboxMode, readCodexRolloutTokenUsageForThread, runLoggedProcess, } from './worker/runtime';
6
6
  export type { AgentTokenUsage, LoggedProcessResult, LoggedProcessTranscriptChunk, } from './worker/runtime';
7
7
  export declare const WORKER_SESSION_EXPIRED_MESSAGE = "worker session expired: run \"playdrop auth login\" and start the worker again";
8
- export declare const 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/capture, read-only catalogue/documentation lookup, and PlayDrop AI generation are permitted.";
8
+ export declare const 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/capture, read-only help/catalogue/documentation lookup, and PlayDrop AI generation are permitted.";
9
9
  type WorkerStartOptions = {
10
10
  env?: string;
11
11
  once?: boolean;
@@ -77,6 +77,7 @@ type TaskCatalogueReportOptions = {
77
77
  type TaskCompleteOptions = {
78
78
  env?: string;
79
79
  nextSteps?: string;
80
+ summary?: string;
80
81
  };
81
82
  type TaskClaimSlugOptions = {
82
83
  appName?: string;
@@ -98,6 +99,10 @@ type TaskSubmitReviewOptions = {
98
99
  evidenceDir?: string;
99
100
  nextSteps?: string;
100
101
  };
102
+ type TaskSubmitEvalOptions = {
103
+ env?: string;
104
+ resultFile?: string;
105
+ };
101
106
  export type WorkerHealthAlertInput = {
102
107
  state: 'started' | 'stopped' | 'crashed';
103
108
  env: string;
@@ -157,6 +162,10 @@ export declare function stagePlaydropPluginReferences(input: {
157
162
  pluginRoot: string;
158
163
  kind: AgentTaskKind;
159
164
  }): Promise<string[]>;
165
+ export declare function stageAssignmentPluginBundle(input: {
166
+ workspaceDir: string;
167
+ pluginBundle: NonNullable<WorkerAgentTaskAssignmentResponse['pluginBundle']>;
168
+ }): Promise<string>;
160
169
  export declare function discoverWorkerProjectRoot(workspaceDir: string): string;
161
170
  export declare function readWorkerTaskState(): WorkerTaskState | null;
162
171
  export declare function buildWorkerHealthAlertText(input: WorkerHealthAlertInput): string;
@@ -181,6 +190,7 @@ export declare function buildWorkerCapabilities(input: string | {
181
190
  } | null;
182
191
  maxParallelTasks?: number | null;
183
192
  runningTaskCount?: number | null;
193
+ providerAccountLabel?: string | null;
184
194
  }): AgentWorkerCapabilities;
185
195
  export declare function buildWorkerClaimBody(input: {
186
196
  workerKey: string;
@@ -254,4 +264,5 @@ export declare function readReviewEvidenceFiles(evidenceDir: string | undefined)
254
264
  contentBase64: string;
255
265
  }>;
256
266
  export declare function submitReviewTask(options: TaskSubmitReviewOptions): Promise<void>;
267
+ export declare function submitEvalTask(options: TaskSubmitEvalOptions): Promise<void>;
257
268
  export declare function failTask(options: TaskFailOptions): Promise<void>;