@playdrop/playdrop-cli 0.17.2 → 0.17.5

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 (34) hide show
  1. package/config/client-meta.json +4 -4
  2. package/dist/apps/build.js +24 -4
  3. package/dist/apps/upload.js +31 -14
  4. package/dist/assetSpecs.js +2 -0
  5. package/dist/commands/upload-content.js +2 -0
  6. package/dist/commands/upload.js +3 -3
  7. package/dist/commands/worker/classification.d.ts +1 -0
  8. package/dist/commands/worker/classification.js +57 -0
  9. package/dist/commands/worker/game-source-commands.d.ts +18 -0
  10. package/dist/commands/worker/game-source-commands.js +182 -0
  11. package/dist/commands/worker/game-source-git.d.ts +1 -0
  12. package/dist/commands/worker/game-source-git.js +44 -16
  13. package/dist/commands/worker/generation.d.ts +3 -0
  14. package/dist/commands/worker/generation.js +71 -21
  15. package/dist/commands/worker.js +352 -249
  16. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts.map +1 -1
  17. package/node_modules/@playdrop/api-client/dist/domains/chat.js +7 -2
  18. package/node_modules/@playdrop/config/client-meta.json +4 -4
  19. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  20. package/node_modules/@playdrop/types/dist/agent-task-classification.d.ts +68 -0
  21. package/node_modules/@playdrop/types/dist/agent-task-classification.d.ts.map +1 -0
  22. package/node_modules/@playdrop/types/dist/agent-task-classification.js +96 -0
  23. package/node_modules/@playdrop/types/dist/api.d.ts +2 -0
  24. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  25. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  26. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  27. package/node_modules/@playdrop/types/dist/index.js +2 -0
  28. package/package.json +2 -2
  29. package/node_modules/@playwright/mcp/node_modules/fsevents/LICENSE +0 -22
  30. package/node_modules/@playwright/mcp/node_modules/fsevents/README.md +0 -83
  31. package/node_modules/@playwright/mcp/node_modules/fsevents/fsevents.d.ts +0 -46
  32. package/node_modules/@playwright/mcp/node_modules/fsevents/fsevents.js +0 -82
  33. package/node_modules/@playwright/mcp/node_modules/fsevents/fsevents.node +0 -0
  34. package/node_modules/@playwright/mcp/node_modules/fsevents/package.json +0 -62
@@ -150,6 +150,7 @@ const provider_telemetry_1 = require("./worker/provider-telemetry");
150
150
  const archive_staging_1 = require("./worker/archive-staging");
151
151
  const game_server_infra_1 = require("./worker/game-server-infra");
152
152
  const game_source_git_1 = require("./worker/game-source-git");
153
+ const game_source_commands_1 = require("./worker/game-source-commands");
153
154
  const generation_1 = require("./worker/generation");
154
155
  const e2e_fixtures_1 = require("./worker/e2e-fixtures");
155
156
  var archive_staging_2 = require("./worker/archive-staging");
@@ -1241,9 +1242,6 @@ function workerTaskUploadFailurePath(workspaceDir) {
1241
1242
  function workerTaskMaterialsPath(workspaceDir) {
1242
1243
  return node_path_1.default.join(workspaceDir, ".playdrop", "task-materials.json");
1243
1244
  }
1244
- function workerGameSourceReadinessPath(workspaceDir) {
1245
- return node_path_1.default.join(workspaceDir, ".playdrop", "game-source-ready.json");
1246
- }
1247
1245
  function workerGameSourceCompletionPath(workspaceDir) {
1248
1246
  return node_path_1.default.join(workspaceDir, ".playdrop", "game-source-completion.json");
1249
1247
  }
@@ -5636,6 +5634,11 @@ async function startWorker(options = {}) {
5636
5634
  let telemetryReported = false;
5637
5635
  let gameServerDevLease = null;
5638
5636
  let gameSourceWorkspace = null;
5637
+ let gameSourceCommandServer = null;
5638
+ let gameSourcePublication = null;
5639
+ let previousGameSourceCommitSha;
5640
+ let preparedGameSourceProposal = null;
5641
+ let completedGameSourceGeneration = null;
5639
5642
  let transcriptSpoolPath = null;
5640
5643
  let transcriptSpoolSequence = 0;
5641
5644
  let heartbeatTransientFailures = 0;
@@ -5643,6 +5646,8 @@ async function startWorker(options = {}) {
5643
5646
  let automaticContinuationAttempted = false;
5644
5647
  let workerFailureStage = "setup";
5645
5648
  const sendTaskHeartbeat = async () => {
5649
+ if (completedGameSourceGeneration)
5650
+ return;
5646
5651
  try {
5647
5652
  const heartbeat = await client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken });
5648
5653
  heartbeatTransientFailures = 0;
@@ -5708,6 +5713,8 @@ async function startWorker(options = {}) {
5708
5713
  startedAt: agentStartedAt ?? completedAt,
5709
5714
  completedAt,
5710
5715
  ...(terminalReason ? { terminalReason } : {}),
5716
+ }).catch((error) => {
5717
+ console.error(`Agent task ${task.id} telemetry could not be recorded: ${error instanceof Error ? error.message : String(error)}`);
5711
5718
  });
5712
5719
  telemetryReported = true;
5713
5720
  };
@@ -5803,6 +5810,255 @@ async function startWorker(options = {}) {
5803
5810
  });
5804
5811
  await reportTelemetry("FAILED", undefined, terminalReason);
5805
5812
  };
5813
+ // The supervisor keeps the lease, but both commands wait for the real
5814
+ // operation while the agent can still read errors, repair, and retry.
5815
+ const publishGameSourceForAgent = async () => {
5816
+ if (gameSourcePublication)
5817
+ return gameSourcePublication.upload;
5818
+ if (fenced || shuttingDown)
5819
+ throw new Error("agent_task_lease_invalid");
5820
+ if (!gameSourceWorkspace || !workspaceDir || !agentWorkspaceDir) {
5821
+ throw new Error("game_source_workspace_missing");
5822
+ }
5823
+ const currentTask = await fetchTaskDetail(client, target, task.id, workerKey);
5824
+ if (currentTask.task.status !== "RUNNING")
5825
+ throw new Error("agent_task_not_running");
5826
+ if (task.kind === "STATIC_GAME" && !(0, node_fs_1.existsSync)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"))) {
5827
+ const primarySurface = resolveWorkerInstrumentSurface(taskContext);
5828
+ if (!primarySurface)
5829
+ throw new Error("static_game_artifact_primary_surface_missing");
5830
+ if (!envConfig.cdnBase)
5831
+ throw new Error("static_game_publish_cdn_base_missing");
5832
+ const preparedStatic = await (0, staticHtml_1.prepareStaticHtmlProject)({
5833
+ projectDir: agentWorkspaceDir,
5834
+ taskId: task.id,
5835
+ appName: currentTask.task.claimedAppName ?? undefined,
5836
+ primarySurface,
5837
+ cdnBase: envConfig.cdnBase,
5838
+ });
5839
+ try {
5840
+ const sourceTask = preparedStatic.sourceTask;
5841
+ await (0, promises_1.writeFile)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"), `${JSON.stringify({
5842
+ apps: [{
5843
+ name: sourceTask.name,
5844
+ version: sourceTask.version,
5845
+ displayName: sourceTask.displayName,
5846
+ subtitle: sourceTask.subtitle,
5847
+ description: sourceTask.description,
5848
+ type: "GAME",
5849
+ authMode: "NONE",
5850
+ controllerMode: "UNSUPPORTED",
5851
+ previewable: true,
5852
+ file: "index.html",
5853
+ primarySurface,
5854
+ surfaceTargets: {
5855
+ desktop: sourceTask.surfaceTargets.includes("DESKTOP"),
5856
+ mobileLandscape: sourceTask.surfaceTargets.includes("MOBILE_LANDSCAPE"),
5857
+ mobilePortrait: sourceTask.surfaceTargets.includes("MOBILE_PORTRAIT"),
5858
+ },
5859
+ releaseNotes: sourceTask.releaseNotes,
5860
+ hostingMode: "HOSTED",
5861
+ license: "CLOSED",
5862
+ }],
5863
+ }, null, 2)}\n`, "utf8");
5864
+ }
5865
+ finally {
5866
+ await preparedStatic.cleanup();
5867
+ }
5868
+ }
5869
+ const outputProjectDir = discoverWorkerProjectRoot(agentWorkspaceDir);
5870
+ if (node_path_1.default.resolve(outputProjectDir) !== node_path_1.default.resolve(gameSourceWorkspace.projectDir)) {
5871
+ throw new Error("game_source_output_must_be_repository_root");
5872
+ }
5873
+ workerFailureStage = "game_source_reconcile";
5874
+ const reconciled = await (0, game_source_git_1.reconcileGameSourceWorkspace)({
5875
+ workspace: gameSourceWorkspace,
5876
+ taskId: task.id,
5877
+ summary: `Prepared ${taskContext.outputAppName ?? "game"} for publication`,
5878
+ previousCommitSha: previousGameSourceCommitSha,
5879
+ });
5880
+ previousGameSourceCommitSha = reconciled.commitSha;
5881
+ const taskClient = (0, apiClient_1.createCliApiClient)({
5882
+ baseUrl: envConfig.apiBase,
5883
+ tokenProvider: ctx.getToken,
5884
+ onBehalfCreatorUsername: taskContext.creatorUsername,
5885
+ agentTaskToken: assignment.task.token,
5886
+ agentTaskId: task.id,
5887
+ agentTaskAttempt: taskContext.attempt,
5888
+ });
5889
+ let published;
5890
+ let finalizedGameSource = null;
5891
+ let preparedGameSource = null;
5892
+ const prepareGitTransition = async (input) => {
5893
+ const versionParts = input.version.split(".").map((part) => Number(part));
5894
+ if (versionParts.length !== 3 ||
5895
+ versionParts.some((part) => !Number.isSafeInteger(part) || part < 0)) {
5896
+ throw new Error("game_source_version_invalid");
5897
+ }
5898
+ // A finalize response can be lost after the version is saved. An
5899
+ // unchanged retry must keep that exact source transition, not a
5900
+ // new timestamped commit or bundle. This cache is attempt-local.
5901
+ if (preparedGameSourceProposal?.appId === input.appId &&
5902
+ preparedGameSourceProposal.version === input.version &&
5903
+ preparedGameSourceProposal.finalized.commitSha === reconciled.commitSha) {
5904
+ finalizedGameSource = preparedGameSourceProposal.finalized;
5905
+ }
5906
+ else {
5907
+ finalizedGameSource = await (0, game_source_git_1.finalizeGameSourceRepository)({
5908
+ reconciled,
5909
+ appVersionId: 0,
5910
+ version: { major: versionParts[0], minor: versionParts[1], patch: versionParts[2] },
5911
+ });
5912
+ preparedGameSourceProposal = {
5913
+ appId: input.appId,
5914
+ version: input.version,
5915
+ finalized: finalizedGameSource,
5916
+ };
5917
+ }
5918
+ // Recheck ownership and generation for this upload session, even
5919
+ // when the immutable bundle is reused after an aborted session.
5920
+ preparedGameSource = await (0, game_source_git_1.uploadFinalizedGameSource)({
5921
+ client,
5922
+ taskId: task.id,
5923
+ attempt: taskContext.attempt,
5924
+ workerKey,
5925
+ leaseToken,
5926
+ appId: input.appId,
5927
+ appUploadSessionId: input.sessionId,
5928
+ source: finalizedGameSource,
5929
+ });
5930
+ return {
5931
+ commitSha: preparedGameSource.commitSha,
5932
+ headSha: preparedGameSource.headSha,
5933
+ baseGeneration: preparedGameSource.baseGeneration,
5934
+ bundleStorageKey: preparedGameSource.bundleStorageKey,
5935
+ bundleSha256: preparedGameSource.bundleSha256,
5936
+ bundleSizeBytes: preparedGameSource.bundleBytes,
5937
+ };
5938
+ };
5939
+ workerFailureStage = "game_publish";
5940
+ if (task.kind === "STATIC_GAME") {
5941
+ const primarySurface = resolveWorkerInstrumentSurface(taskContext);
5942
+ if (!primarySurface)
5943
+ throw new Error("static_game_artifact_primary_surface_missing");
5944
+ if (!envConfig.cdnBase)
5945
+ throw new Error("static_game_publish_cdn_base_missing");
5946
+ const result = await (0, upload_1.publishStaticHtmlProject)({
5947
+ client: taskClient,
5948
+ taskId: task.id,
5949
+ taskToken: assignment.task.token,
5950
+ projectDir: agentWorkspaceDir,
5951
+ creatorUsername: taskContext.creatorUsername,
5952
+ primarySurface,
5953
+ apiBase: envConfig.apiBase,
5954
+ webBase: envConfig.webBase ?? null,
5955
+ cdnBase: envConfig.cdnBase,
5956
+ claimedAppName: currentTask.task.claimedAppName,
5957
+ devRouterPort: devPort,
5958
+ token: ctx.getToken(),
5959
+ user: me.user,
5960
+ prepareGitTransition,
5961
+ });
5962
+ published = result;
5963
+ }
5964
+ else {
5965
+ const fixtureKind = (0, e2e_fixtures_1.detectPlaydropE2EFixture)(assignment.request.prompt);
5966
+ const result = await (0, upload_1.publishWorkerAppProject)({
5967
+ client: taskClient,
5968
+ taskId: task.id,
5969
+ taskToken: assignment.task.token,
5970
+ kind: task.kind,
5971
+ executionTarget: task.executionTarget,
5972
+ expectedAppName: taskContext.outputAppName ?? undefined,
5973
+ expectedDisplayName: taskContext.outputDisplayName,
5974
+ expectedSubtitle: taskContext.outputSubtitle,
5975
+ expectedPrimarySurface: taskContext.outputPrimarySurface,
5976
+ remixSourceRef: taskContext.remixSourceRef ?? null,
5977
+ projectDir: agentWorkspaceDir,
5978
+ creatorUsername: taskContext.creatorUsername,
5979
+ apiBase: envConfig.apiBase,
5980
+ webBase: envConfig.webBase ?? null,
5981
+ token: ctx.getToken(),
5982
+ user: me.user,
5983
+ devRouterPort: devPort,
5984
+ deterministicE2EFixture: fixtureKind === task.kind,
5985
+ prepareGitTransition,
5986
+ });
5987
+ published = result;
5988
+ }
5989
+ if (!finalizedGameSource || !preparedGameSource) {
5990
+ throw new Error("game_source_transition_not_prepared");
5991
+ }
5992
+ const finalized = finalizedGameSource;
5993
+ const gameSource = preparedGameSource;
5994
+ if (!published.versionNodeId)
5995
+ throw new Error("game_source_version_node_missing");
5996
+ const upload = {
5997
+ taskId: task.id,
5998
+ appId: published.appId,
5999
+ appVersionId: published.appVersionId,
6000
+ appName: published.appName,
6001
+ version: published.version,
6002
+ versionNodeId: published.versionNodeId,
6003
+ creatorUsername: taskContext.creatorUsername,
6004
+ };
6005
+ gameSourcePublication = { upload, source: gameSource, finalized };
6006
+ await (0, promises_1.writeFile)(workerTaskUploadResultPath(workspaceDir), JSON.stringify(upload, null, 2), "utf8");
6007
+ await (0, promises_1.rm)(workerTaskUploadFailurePath(workspaceDir), { force: true });
6008
+ for (const warning of published.warnings)
6009
+ console.error(`Upload warning: ${warning}`);
6010
+ return upload;
6011
+ };
6012
+ const completeGameSourceForAgent = async (intent) => {
6013
+ if (completedGameSourceGeneration)
6014
+ return;
6015
+ if (fenced || shuttingDown)
6016
+ throw new Error("agent_task_lease_invalid");
6017
+ if (!gameSourcePublication)
6018
+ throw new Error("task_done_missing_upload_result: Run playdrop task upload successfully first.");
6019
+ if (intent.taskId !== task.id || intent.attempt !== taskContext.attempt ||
6020
+ typeof intent.summary !== "string" || !intent.summary.trim() ||
6021
+ intent.summary.length > 200 || /[\r\n]/.test(intent.summary)) {
6022
+ throw new Error("game_source_completion_intent_invalid");
6023
+ }
6024
+ const { upload: published, source: gameSource, finalized } = gameSourcePublication;
6025
+ workerFailureStage = "task_complete";
6026
+ const completion = await completeWorkerAgentTaskWithRetry({
6027
+ client,
6028
+ taskId: task.id,
6029
+ body: {
6030
+ workerKey,
6031
+ leaseToken,
6032
+ appId: published.appId,
6033
+ appVersionId: published.appVersionId,
6034
+ result: {
6035
+ appId: published.appId,
6036
+ appVersionId: published.appVersionId,
6037
+ appName: published.appName,
6038
+ version: published.version,
6039
+ versionNodeId: published.versionNodeId,
6040
+ completedBy: "worker_game_source_git",
6041
+ },
6042
+ summary: intent.summary,
6043
+ ...(intent.nextSteps ? { nextSteps: intent.nextSteps } : {}),
6044
+ ...(intent.platformFeedback ? { platformFeedback: intent.platformFeedback } : {}),
6045
+ ...(agentResult ? { tokensUsed: agentResult.tokensUsed } : {}),
6046
+ gameSource,
6047
+ },
6048
+ });
6049
+ if (!completion.gameSource)
6050
+ throw new Error("game_source_completion_metadata_missing");
6051
+ completedGameSourceGeneration = completion.gameSource.repoGeneration;
6052
+ (0, game_source_git_1.emitGameSourceWorkerEvent)("playdrop.game_source.completed", {
6053
+ taskId: task.id,
6054
+ appId: published.appId,
6055
+ repoGeneration: completion.gameSource.repoGeneration,
6056
+ bundleBytes: finalized.bundle.length,
6057
+ materialCount: finalized.materials.length,
6058
+ outcome: "DONE",
6059
+ });
6060
+ retainWorkspace = false;
6061
+ };
5806
6062
  try {
5807
6063
  await sendTaskHeartbeat();
5808
6064
  if (sessionExpired) {
@@ -5896,13 +6152,18 @@ async function startWorker(options = {}) {
5896
6152
  devPort,
5897
6153
  gameSourceGit: Boolean(assignment.gameSource),
5898
6154
  });
5899
- const handleEventDrainFailure = (error) => {
6155
+ const handleEventDrainFailure = async (error) => {
6156
+ if (completedGameSourceGeneration)
6157
+ return;
5900
6158
  const classification = classifyWorkerEventDrainError(error);
5901
6159
  if (classification === "session_expired") {
5902
6160
  handleSessionExpiry();
5903
6161
  return;
5904
6162
  }
5905
6163
  if (classification === "lease_invalid") {
6164
+ const current = await fetchTaskDetail(client, target, task.id, workerKey).catch(() => null);
6165
+ if (current?.task.status === "DONE")
6166
+ return;
5906
6167
  fenced = true;
5907
6168
  activeTerminators.get(task.id)?.();
5908
6169
  return;
@@ -5910,7 +6171,10 @@ async function startWorker(options = {}) {
5910
6171
  console.error(`Agent task event drain failed: ${error instanceof Error ? error.message : String(error)}`);
5911
6172
  };
5912
6173
  const queueEventDrain = () => {
5913
- eventDrainPromise = eventDrainPromise.then(() => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }), () => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }));
6174
+ const drain = () => completedGameSourceGeneration
6175
+ ? Promise.resolve()
6176
+ : drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken });
6177
+ eventDrainPromise = eventDrainPromise.then(drain, drain);
5914
6178
  return eventDrainPromise;
5915
6179
  };
5916
6180
  const appendObservedSkillPathsFromTranscript = (chunks) => {
@@ -6041,7 +6305,7 @@ async function startWorker(options = {}) {
6041
6305
  }
6042
6306
  eventDrainTimer = setInterval(() => {
6043
6307
  queueEventDrain().catch((error) => {
6044
- handleEventDrainFailure(error);
6308
+ return handleEventDrainFailure(error);
6045
6309
  });
6046
6310
  }, 1000);
6047
6311
  const gameCreationTask = usesCreatorRecovery(task.kind);
@@ -6078,7 +6342,7 @@ async function startWorker(options = {}) {
6078
6342
  try {
6079
6343
  enqueueWorkerEvent(eventDir, initialProgress);
6080
6344
  queueEventDrain().catch((error) => {
6081
- handleEventDrainFailure(error);
6345
+ return handleEventDrainFailure(error);
6082
6346
  });
6083
6347
  }
6084
6348
  catch (error) {
@@ -6223,6 +6487,31 @@ async function startWorker(options = {}) {
6223
6487
  message: "Resumed the retained game workspace with a focused creation recovery window.",
6224
6488
  });
6225
6489
  }
6490
+ if (assignment.gameSource && task.kind !== "STATIC_GAME") {
6491
+ gameSourceCommandServer = await (0, game_source_commands_1.startWorkerGameSourceCommandServer)({
6492
+ workspaceDir,
6493
+ taskId: task.id,
6494
+ attempt: taskContext.attempt,
6495
+ onCommand: async (request) => {
6496
+ try {
6497
+ if (request.command === "upload")
6498
+ return await publishGameSourceForAgent();
6499
+ if (!request.payload || typeof request.payload !== "object") {
6500
+ throw new Error("game_source_completion_intent_invalid");
6501
+ }
6502
+ await completeGameSourceForAgent(request.payload);
6503
+ return { completed: true };
6504
+ }
6505
+ catch (error) {
6506
+ await recordTaskUploadFailure({ workspaceDir: workspaceDir, taskContext, error }).catch((recordError) => console.error(`Could not record task command failure: ${recordError}`));
6507
+ throw error;
6508
+ }
6509
+ finally {
6510
+ workerFailureStage = "agent";
6511
+ }
6512
+ },
6513
+ });
6514
+ }
6226
6515
  workerFailureStage = "agent";
6227
6516
  agentResult = await runAssignedAgent({
6228
6517
  prompt,
@@ -6314,10 +6603,12 @@ async function startWorker(options = {}) {
6314
6603
  }
6315
6604
  }
6316
6605
  }
6606
+ await gameSourceCommandServer?.close();
6607
+ gameSourceCommandServer = null;
6317
6608
  resolvedRuntimeModel = agentResult.providerResolvedModel?.trim() || resolvedRuntimeModel;
6318
6609
  agentCompletedAt = new Date();
6319
6610
  await queueEventDrain().catch((error) => {
6320
- handleEventDrainFailure(error);
6611
+ return handleEventDrainFailure(error);
6321
6612
  });
6322
6613
  void archiveWorkerTranscriptBestEffort({
6323
6614
  client,
@@ -6461,216 +6752,16 @@ async function startWorker(options = {}) {
6461
6752
  await failMissingAgentOutcome(agentRunResult);
6462
6753
  }
6463
6754
  else if (assignment.gameSource) {
6464
- if (!gameSourceWorkspace || !workspaceDir || !agentWorkspaceDir) {
6465
- throw new Error("game_source_workspace_missing");
6466
- }
6467
- const intent = task.kind === "STATIC_GAME"
6468
- ? {
6469
- taskId: task.id,
6470
- attempt: taskContext.attempt,
6471
- summary: "Created and privately hosted a standalone static web game.",
6472
- }
6473
- : readWorkerGameSourceCompletion(workspaceDir);
6474
- if (!intent || intent.taskId !== task.id || intent.attempt !== taskContext.attempt) {
6475
- throw new Error("agent_exited_without_task_done");
6755
+ if (task.kind !== "STATIC_GAME") {
6756
+ throw new Error("agent_exited_without_successful_task_done");
6476
6757
  }
6477
- if (task.kind !== "STATIC_GAME" && !(0, node_fs_1.existsSync)(workerGameSourceReadinessPath(workspaceDir))) {
6478
- throw new Error("agent_exited_without_task_upload");
6479
- }
6480
- if (task.kind === "STATIC_GAME" && !(0, node_fs_1.existsSync)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"))) {
6481
- const primarySurface = resolveWorkerInstrumentSurface(taskContext);
6482
- if (!primarySurface)
6483
- throw new Error("static_game_artifact_primary_surface_missing");
6484
- if (!envConfig.cdnBase)
6485
- throw new Error("static_game_publish_cdn_base_missing");
6486
- const preparedStatic = await (0, staticHtml_1.prepareStaticHtmlProject)({
6487
- projectDir: agentWorkspaceDir,
6488
- taskId: task.id,
6489
- appName: refreshed.task.claimedAppName ?? undefined,
6490
- primarySurface,
6491
- cdnBase: envConfig.cdnBase,
6492
- });
6493
- try {
6494
- const sourceTask = preparedStatic.sourceTask;
6495
- await (0, promises_1.writeFile)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"), `${JSON.stringify({
6496
- apps: [{
6497
- name: sourceTask.name,
6498
- version: sourceTask.version,
6499
- displayName: sourceTask.displayName,
6500
- subtitle: sourceTask.subtitle,
6501
- description: sourceTask.description,
6502
- type: "GAME",
6503
- authMode: "NONE",
6504
- controllerMode: "UNSUPPORTED",
6505
- previewable: true,
6506
- file: "index.html",
6507
- primarySurface,
6508
- surfaceTargets: {
6509
- desktop: sourceTask.surfaceTargets.includes("DESKTOP"),
6510
- mobileLandscape: sourceTask.surfaceTargets.includes("MOBILE_LANDSCAPE"),
6511
- mobilePortrait: sourceTask.surfaceTargets.includes("MOBILE_PORTRAIT"),
6512
- },
6513
- releaseNotes: sourceTask.releaseNotes,
6514
- hostingMode: "HOSTED",
6515
- license: "CLOSED",
6516
- }],
6517
- }, null, 2)}\n`, "utf8");
6518
- }
6519
- finally {
6520
- await preparedStatic.cleanup();
6521
- }
6522
- }
6523
- const outputProjectDir = discoverWorkerProjectRoot(agentWorkspaceDir);
6524
- if (node_path_1.default.resolve(outputProjectDir) !== node_path_1.default.resolve(gameSourceWorkspace.projectDir)) {
6525
- throw new Error("game_source_output_must_be_repository_root");
6526
- }
6527
- workerFailureStage = "game_source_reconcile";
6528
- const reconciled = await (0, game_source_git_1.reconcileGameSourceWorkspace)({
6529
- workspace: gameSourceWorkspace,
6758
+ await publishGameSourceForAgent();
6759
+ await completeGameSourceForAgent({
6530
6760
  taskId: task.id,
6531
- summary: intent.summary,
6532
- });
6533
- const taskClient = (0, apiClient_1.createCliApiClient)({
6534
- baseUrl: envConfig.apiBase,
6535
- tokenProvider: ctx.getToken,
6536
- onBehalfCreatorUsername: taskContext.creatorUsername,
6537
- agentTaskToken: assignment.task.token,
6538
- agentTaskId: task.id,
6539
- agentTaskAttempt: taskContext.attempt,
6540
- });
6541
- let published;
6542
- let finalizedGameSource = null;
6543
- let preparedGameSource = null;
6544
- const prepareGitTransition = async (input) => {
6545
- const versionParts = input.version.split(".").map((part) => Number(part));
6546
- if (versionParts.length !== 3 ||
6547
- versionParts.some((part) => !Number.isSafeInteger(part) || part < 0)) {
6548
- throw new Error("game_source_version_invalid");
6549
- }
6550
- finalizedGameSource = await (0, game_source_git_1.finalizeGameSourceRepository)({
6551
- reconciled,
6552
- appVersionId: 0,
6553
- version: { major: versionParts[0], minor: versionParts[1], patch: versionParts[2] },
6554
- });
6555
- preparedGameSource = await (0, game_source_git_1.uploadFinalizedGameSource)({
6556
- client,
6557
- taskId: task.id,
6558
- attempt: taskContext.attempt,
6559
- workerKey,
6560
- leaseToken,
6561
- appId: input.appId,
6562
- appUploadSessionId: input.sessionId,
6563
- source: finalizedGameSource,
6564
- });
6565
- return {
6566
- commitSha: preparedGameSource.commitSha,
6567
- headSha: preparedGameSource.headSha,
6568
- baseGeneration: preparedGameSource.baseGeneration,
6569
- bundleStorageKey: preparedGameSource.bundleStorageKey,
6570
- bundleSha256: preparedGameSource.bundleSha256,
6571
- bundleSizeBytes: preparedGameSource.bundleBytes,
6572
- };
6573
- };
6574
- workerFailureStage = "game_publish";
6575
- if (task.kind === "STATIC_GAME") {
6576
- const primarySurface = resolveWorkerInstrumentSurface(taskContext);
6577
- if (!primarySurface)
6578
- throw new Error("static_game_artifact_primary_surface_missing");
6579
- if (!envConfig.cdnBase)
6580
- throw new Error("static_game_publish_cdn_base_missing");
6581
- const result = await (0, upload_1.publishStaticHtmlProject)({
6582
- client: taskClient,
6583
- taskId: task.id,
6584
- taskToken: assignment.task.token,
6585
- projectDir: agentWorkspaceDir,
6586
- creatorUsername: taskContext.creatorUsername,
6587
- primarySurface,
6588
- apiBase: envConfig.apiBase,
6589
- webBase: envConfig.webBase ?? null,
6590
- cdnBase: envConfig.cdnBase,
6591
- claimedAppName: refreshed.task.claimedAppName,
6592
- devRouterPort: devPort,
6593
- token: ctx.getToken(),
6594
- user: me.user,
6595
- prepareGitTransition,
6596
- });
6597
- published = result;
6598
- }
6599
- else {
6600
- const fixtureKind = (0, e2e_fixtures_1.detectPlaydropE2EFixture)(assignment.request.prompt);
6601
- const result = await (0, upload_1.publishWorkerAppProject)({
6602
- client: taskClient,
6603
- taskId: task.id,
6604
- taskToken: assignment.task.token,
6605
- kind: task.kind,
6606
- executionTarget: task.executionTarget,
6607
- expectedAppName: taskContext.outputAppName ?? undefined,
6608
- expectedDisplayName: taskContext.outputDisplayName,
6609
- expectedSubtitle: taskContext.outputSubtitle,
6610
- expectedPrimarySurface: taskContext.outputPrimarySurface,
6611
- remixSourceRef: taskContext.remixSourceRef ?? null,
6612
- projectDir: agentWorkspaceDir,
6613
- creatorUsername: taskContext.creatorUsername,
6614
- apiBase: envConfig.apiBase,
6615
- webBase: envConfig.webBase ?? null,
6616
- token: ctx.getToken(),
6617
- user: me.user,
6618
- devRouterPort: devPort,
6619
- deterministicE2EFixture: fixtureKind === task.kind,
6620
- prepareGitTransition,
6621
- });
6622
- published = result;
6623
- }
6624
- if (!finalizedGameSource || !preparedGameSource) {
6625
- throw new Error("game_source_transition_not_prepared");
6626
- }
6627
- const finalized = finalizedGameSource;
6628
- const gameSource = preparedGameSource;
6629
- workerFailureStage = "task_complete";
6630
- const completion = await completeWorkerAgentTaskWithRetry({
6631
- client,
6632
- taskId: task.id,
6633
- body: {
6634
- workerKey,
6635
- leaseToken,
6636
- appId: published.appId,
6637
- appVersionId: published.appVersionId,
6638
- result: {
6639
- appId: published.appId,
6640
- appVersionId: published.appVersionId,
6641
- appName: published.appName,
6642
- version: published.version,
6643
- versionNodeId: published.versionNodeId,
6644
- completedBy: "worker_game_source_git",
6645
- },
6646
- summary: intent.summary,
6647
- ...(intent.nextSteps ? { nextSteps: intent.nextSteps } : {}),
6648
- ...(intent.platformFeedback ? { platformFeedback: intent.platformFeedback } : {}),
6649
- tokensUsed: agentResult.tokensUsed,
6650
- gameSource,
6651
- },
6652
- });
6653
- if (!completion.gameSource)
6654
- throw new Error("game_source_completion_metadata_missing");
6655
- (0, game_source_git_1.emitGameSourceWorkerEvent)("playdrop.game_source.completed", {
6656
- taskId: task.id,
6657
- appId: published.appId,
6658
- repoGeneration: completion.gameSource.repoGeneration,
6659
- bundleBytes: finalized.bundle.length,
6660
- materialCount: finalized.materials.length,
6661
- outcome: "DONE",
6761
+ attempt: taskContext.attempt,
6762
+ summary: "Created and privately hosted a standalone static web game.",
6662
6763
  });
6663
6764
  await reportTelemetry("DONE");
6664
- await (0, game_source_git_1.cleanupCompletedGameSourceWorkspace)({
6665
- workspace: gameSourceWorkspace,
6666
- workerHomeDir: resolveWorkerHomeDir(),
6667
- appId: published.appId,
6668
- repoGeneration: completion.gameSource.repoGeneration,
6669
- headSha: finalized.commitSha,
6670
- versions: finalized.versions,
6671
- });
6672
- retainWorkspace = false;
6673
- console.log(`Agent task ${task.id} reconciled, published, and completed with canonical Git source.`);
6674
6765
  }
6675
6766
  else if (task.kind === "STATIC_GAME") {
6676
6767
  await reportTelemetry("DONE");
@@ -6843,6 +6934,8 @@ async function startWorker(options = {}) {
6843
6934
  }
6844
6935
  catch (error) {
6845
6936
  const message = error instanceof Error ? error.message : String(error);
6937
+ await gameSourceCommandServer?.close();
6938
+ gameSourceCommandServer = null;
6846
6939
  if (gameSourceWorkspace) {
6847
6940
  await (0, game_source_git_1.checkpointFailedGameSourceWorkspace)({ workspace: gameSourceWorkspace, taskId: task.id }).catch((checkpointError) => {
6848
6941
  console.error(`Failed Git workspace checkpoint did not complete: ${checkpointError instanceof Error ? checkpointError.message : String(checkpointError)}`);
@@ -6900,6 +6993,7 @@ async function startWorker(options = {}) {
6900
6993
  }
6901
6994
  }
6902
6995
  finally {
6996
+ await gameSourceCommandServer?.close();
6903
6997
  if (heartbeatTimer) {
6904
6998
  clearInterval(heartbeatTimer);
6905
6999
  }
@@ -6908,6 +7002,20 @@ async function startWorker(options = {}) {
6908
7002
  }
6909
7003
  activeTerminators.delete(task.id);
6910
7004
  await gameServerDevLease?.cleanup();
7005
+ if (completedGameSourceGeneration && gameSourcePublication && gameSourceWorkspace) {
7006
+ const publication = gameSourcePublication;
7007
+ await (0, game_source_git_1.cleanupCompletedGameSourceWorkspace)({
7008
+ workspace: gameSourceWorkspace,
7009
+ workerHomeDir: resolveWorkerHomeDir(),
7010
+ appId: publication.upload.appId,
7011
+ repoGeneration: completedGameSourceGeneration,
7012
+ headSha: publication.finalized.commitSha,
7013
+ versions: publication.finalized.versions,
7014
+ }).catch((cleanupError) => {
7015
+ retainWorkspace = true;
7016
+ console.error(`Completed game source cache cleanup failed: ${cleanupError}`);
7017
+ });
7018
+ }
6911
7019
  if (workspaceDir) {
6912
7020
  await removeAgentRunCredentials(workspaceDir);
6913
7021
  }
@@ -7424,37 +7532,20 @@ async function uploadTask(options = {}) {
7424
7532
  return;
7425
7533
  }
7426
7534
  if (taskContext.gameSourceGit) {
7427
- const ctx = await resolveTaskCommandContext("task upload", options.env, taskContext);
7428
- if (!ctx)
7429
- return;
7430
- const projectDir = discoverWorkerProjectRoot(node_process_1.default.cwd());
7431
- const result = await (0, upload_1.preflightWorkerAppProject)({
7432
- client: ctx.client,
7433
- taskId: taskContext.taskId,
7434
- taskToken: taskContext.taskToken,
7435
- kind: uploadKind,
7436
- executionTarget: taskContext.target,
7437
- expectedAppName: taskContext.outputAppName ?? undefined,
7438
- expectedDisplayName: taskContext.outputDisplayName,
7439
- expectedSubtitle: taskContext.outputSubtitle,
7440
- expectedPrimarySurface: taskContext.outputPrimarySurface,
7441
- remixSourceRef: taskContext.remixSourceRef ?? null,
7442
- projectDir,
7443
- creatorUsername: taskContext.creatorUsername,
7444
- apiBase: ctx.envConfig.apiBase,
7445
- webBase: ctx.envConfig.webBase ?? null,
7446
- token: ctx.token,
7447
- user: ctx.user,
7535
+ await withTaskUploadLock(workspaceDir, async () => {
7536
+ const ctx = await resolveTaskCommandContext("task upload", options.env, taskContext);
7537
+ if (!ctx)
7538
+ return;
7539
+ await (0, game_source_commands_1.requestWorkerGameSourceCommand)({
7540
+ workspaceDir,
7541
+ taskId: taskContext.taskId,
7542
+ attempt: taskContext.attempt,
7543
+ command: "upload",
7544
+ });
7545
+ const result = readTaskUploadResultFile(workspaceDir);
7546
+ assertTaskUploadResultMatchesContext({ taskContext, uploadResult: result });
7547
+ (0, output_1.printSuccess)(`Uploaded ${result.creatorUsername}/${result.appName} version ${result.version}. Run "playdrop task done" to close the task.`);
7448
7548
  });
7449
- await (0, promises_1.writeFile)(workerGameSourceReadinessPath(workspaceDir), JSON.stringify({
7450
- taskId: taskContext.taskId,
7451
- attempt: taskContext.attempt,
7452
- projectDir,
7453
- readyAt: new Date().toISOString(),
7454
- }, null, 2));
7455
- for (const warning of result.warnings)
7456
- console.error(`Preflight warning: ${warning}`);
7457
- (0, output_1.printSuccess)("Task source is ready. Run \"playdrop task done\" when the work is complete.");
7458
7549
  return;
7459
7550
  }
7460
7551
  if (taskContext.kind === "STATIC_GAME") {
@@ -7586,8 +7677,8 @@ async function completeTask(options) {
7586
7677
  throw new Error("task_done_summary_too_long:max_200_characters");
7587
7678
  }
7588
7679
  if (taskContext.gameSourceGit) {
7589
- if (!(0, node_fs_1.existsSync)(workerGameSourceReadinessPath(workspaceDir)))
7590
- throw new Error("task_done_source_not_ready");
7680
+ const uploadResult = readTaskUploadResultFile(workspaceDir);
7681
+ assertTaskUploadResultMatchesContext({ taskContext, uploadResult });
7591
7682
  const ctx = await resolveTaskCommandContext("task done", options.env, taskContext);
7592
7683
  if (!ctx)
7593
7684
  return;
@@ -7608,8 +7699,20 @@ async function completeTask(options) {
7608
7699
  ...(nextSteps !== undefined ? { nextSteps } : {}),
7609
7700
  ...(platformFeedback ? { platformFeedback } : {}),
7610
7701
  };
7611
- await (0, promises_1.writeFile)(workerGameSourceCompletionPath(workspaceDir), `${JSON.stringify(intent, null, 2)}\n`, "utf8");
7612
- (0, output_1.printSuccess)("Task completion recorded. The worker will reconcile and publish the source after the agent exits.");
7702
+ await withTaskUploadLock(workspaceDir, async () => {
7703
+ const result = await (0, game_source_commands_1.requestWorkerGameSourceCommand)({
7704
+ workspaceDir,
7705
+ taskId: taskContext.taskId,
7706
+ attempt: taskContext.attempt,
7707
+ command: "done",
7708
+ payload: intent,
7709
+ });
7710
+ if (!result || typeof result !== "object" || !("completed" in result) || result.completed !== true) {
7711
+ throw new Error("game_source_completion_response_invalid");
7712
+ }
7713
+ await (0, promises_1.writeFile)(workerGameSourceCompletionPath(workspaceDir), `${JSON.stringify(intent, null, 2)}\n`, "utf8");
7714
+ });
7715
+ (0, output_1.printSuccess)("Task marked done.");
7613
7716
  return;
7614
7717
  }
7615
7718
  const uploadResult = readTaskUploadResultFile();
@@ -7957,7 +8060,7 @@ async function failTask(options) {
7957
8060
  const classifiedMessage = (0, types_1.classifyAgentTaskTerminalReason)(message, "agent");
7958
8061
  const terminalReason = uploadFailure?.terminalReason ?? {
7959
8062
  ...classifiedMessage,
7960
- message: "The agent reported that it could not complete the task.",
8063
+ message,
7961
8064
  retryable: options.retryableWithSameInput === true,
7962
8065
  source: "agent",
7963
8066
  };