@playdrop/playdrop-cli 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/config/client-meta.json +2 -1
  2. package/dist/apiClient.d.ts +8 -0
  3. package/dist/apiClient.js +29 -1
  4. package/dist/captureRuntime.d.ts +13 -0
  5. package/dist/captureRuntime.js +21 -0
  6. package/dist/commandContext.js +21 -3
  7. package/dist/commands/captureRemote.d.ts +2 -0
  8. package/dist/commands/captureRemote.js +90 -0
  9. package/dist/commands/review.d.ts +46 -0
  10. package/dist/commands/review.js +353 -0
  11. package/dist/commands/worker/runtime.d.ts +12 -0
  12. package/dist/commands/worker/runtime.js +79 -35
  13. package/dist/commands/worker.d.ts +17 -3
  14. package/dist/commands/worker.js +431 -24
  15. package/dist/index.js +45 -0
  16. package/dist/workspaceAuth.d.ts +2 -0
  17. package/dist/workspaceAuth.js +6 -0
  18. package/node_modules/@playdrop/api-client/dist/client.d.ts +6 -2
  19. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  20. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +3 -2
  21. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  22. package/node_modules/@playdrop/api-client/dist/domains/admin.js +6 -3
  23. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +4 -1
  24. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +36 -0
  26. package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -2
  27. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/index.js +21 -6
  29. package/node_modules/@playdrop/config/client-meta.json +2 -1
  30. package/node_modules/@playdrop/types/dist/api.d.ts +163 -3
  31. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  32. package/node_modules/@playdrop/types/dist/api.js +11 -1
  33. package/package.json +1 -1
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readCodexSandboxMode = exports.readEnvBoolean = exports.extractCodexTokensUsed = exports.buildWorkerChildEnv = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.buildCodexExecArgs = exports.assertWorkerTokenUsageWithinCap = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = void 0;
6
+ exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readCodexSandboxMode = exports.readEnvBoolean = exports.extractCodexTokensUsed = exports.extractAgentTokenUsage = exports.buildWorkerChildEnv = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.buildCodexExecArgs = exports.assertWorkerTokenUsageWithinCap = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = void 0;
7
7
  exports.allocateWorkerDevPort = allocateWorkerDevPort;
8
8
  exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
9
9
  exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
@@ -39,6 +39,8 @@ exports.reportTask = reportTask;
39
39
  exports.reportCatalogueTask = reportCatalogueTask;
40
40
  exports.uploadTask = uploadTask;
41
41
  exports.completeTask = completeTask;
42
+ exports.readReviewEvidenceFiles = readReviewEvidenceFiles;
43
+ exports.submitReviewTask = submitReviewTask;
42
44
  exports.failTask = failTask;
43
45
  const promises_1 = require("node:fs/promises");
44
46
  const node_fs_1 = require("node:fs");
@@ -55,6 +57,7 @@ const messages_1 = require("../messages");
55
57
  const output_1 = require("../output");
56
58
  const shellProbe_1 = require("../shellProbe");
57
59
  const upload_1 = require("./upload");
60
+ const review_1 = require("./review");
58
61
  const runtime_1 = require("./worker/runtime");
59
62
  var runtime_2 = require("./worker/runtime");
60
63
  Object.defineProperty(exports, "DEFAULT_CODEX_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CODEX_TIMEOUT_MS; } });
@@ -64,6 +67,7 @@ Object.defineProperty(exports, "buildCodexExecArgs", { enumerable: true, get: fu
64
67
  Object.defineProperty(exports, "buildClaudeExecArgs", { enumerable: true, get: function () { return runtime_2.buildClaudeExecArgs; } });
65
68
  Object.defineProperty(exports, "buildClaudePermissionSettings", { enumerable: true, get: function () { return runtime_2.buildClaudePermissionSettings; } });
66
69
  Object.defineProperty(exports, "buildWorkerChildEnv", { enumerable: true, get: function () { return runtime_2.buildWorkerChildEnv; } });
70
+ Object.defineProperty(exports, "extractAgentTokenUsage", { enumerable: true, get: function () { return runtime_2.extractAgentTokenUsage; } });
67
71
  Object.defineProperty(exports, "extractCodexTokensUsed", { enumerable: true, get: function () { return runtime_2.extractCodexTokensUsed; } });
68
72
  Object.defineProperty(exports, "readEnvBoolean", { enumerable: true, get: function () { return runtime_2.readEnvBoolean; } });
69
73
  Object.defineProperty(exports, "readCodexSandboxMode", { enumerable: true, get: function () { return runtime_2.readCodexSandboxMode; } });
@@ -78,13 +82,14 @@ const CLAIM_BACKOFF_JITTER_MS = 500;
78
82
  const FAIL_REPORT_RETRY_DELAY_MS = 2000;
79
83
  const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
80
84
  const SLACK_API_BASE_URL = 'https://slack.com/api';
81
- const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'GAME_UPDATE'];
85
+ const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'GAME_UPDATE', 'GAME_REVIEW'];
82
86
  const requireFromWorker = (0, node_module_1.createRequire)(__filename);
83
87
  const PLAYDROP_PLUGIN_DAEMON_GAME_CREATION_SKILL_PATH = 'skills/daemon-game-creation/SKILL.md';
84
88
  const PLAYDROP_PLUGIN_GAME_IMPROVEMENT_SKILL_PATH = 'skills/game-improvement/SKILL.md';
85
89
  const PLAYDROP_PLUGIN_ASSET_DISCOVERY_SKILL_PATH = 'skills/asset-discovery/SKILL.md';
86
90
  const PLAYDROP_PLUGIN_ASSET_EXTRACTION_SKILL_PATH = 'skills/asset-extraction-2d/SKILL.md';
87
91
  const PLAYDROP_PLUGIN_LISTING_ART_SKILL_PATH = 'skills/listing-art/SKILL.md';
92
+ const PLAYDROP_PLUGIN_GAME_REVIEW_SKILL_PATH = 'skills/game-review/SKILL.md';
88
93
  const PLAYDROP_PLUGIN_ASSET_REUSE_REFERENCE_PATH = 'references/asset-reuse.md';
89
94
  const PLAYDROP_PLUGIN_ASSETS_AND_GENERATION_REFERENCE_PATH = 'references/assets-and-generation.md';
90
95
  const PLAYDROP_PLUGIN_CODE_REUSE_REFERENCE_PATH = 'references/code-reuse.md';
@@ -92,7 +97,7 @@ const PLAYDROP_PLUGIN_EXTRACTOR_PATH = 'scripts/extract-alpha-background-swap.ts
92
97
  const PLAYDROP_PLUGIN_LISTING_ICON_SCRIPT_PATH = 'scripts/compose-listing-icon.ts';
93
98
  const PLAYDROP_PLUGIN_LISTING_TITLE_SCRIPT_PATH = 'scripts/compose-listing-title.ts';
94
99
  const STAGED_PLAYDROP_PLUGIN_ROOT = '.playdrop/plugin';
95
- const PLAYDROP_PLUGIN_REFERENCE_FILES = [
100
+ const PLAYDROP_PLUGIN_CREATION_REFERENCE_FILES = [
96
101
  PLAYDROP_PLUGIN_DAEMON_GAME_CREATION_SKILL_PATH,
97
102
  PLAYDROP_PLUGIN_GAME_IMPROVEMENT_SKILL_PATH,
98
103
  PLAYDROP_PLUGIN_ASSET_DISCOVERY_SKILL_PATH,
@@ -105,13 +110,48 @@ const PLAYDROP_PLUGIN_REFERENCE_FILES = [
105
110
  PLAYDROP_PLUGIN_LISTING_ICON_SCRIPT_PATH,
106
111
  PLAYDROP_PLUGIN_LISTING_TITLE_SCRIPT_PATH,
107
112
  ];
113
+ const PLAYDROP_PLUGIN_GAME_REVIEW_REFERENCE_FILES = [
114
+ PLAYDROP_PLUGIN_GAME_REVIEW_SKILL_PATH,
115
+ 'references/game-review-criteria.md',
116
+ 'references/game-review-comparative-method.md',
117
+ 'references/game-review-evidence-capture.md',
118
+ 'references/game-review-rating-scale.md',
119
+ 'references/game-review-score-caps.md',
120
+ 'references/game-review-gates.md',
121
+ 'references/game-review-gates/concept.md',
122
+ 'references/game-review-gates/gameplay-prototype.md',
123
+ 'references/game-review-gates/demo.md',
124
+ 'references/game-review-gates/vertical-slice.md',
125
+ 'references/game-review-gates/first-release.md',
126
+ 'references/game-review-gates/mature-live-version.md',
127
+ 'references/game-review-outcomes.md',
128
+ 'references/game-review-feedback-format.md',
129
+ 'references/game-review-dimensions/gameplay-core-loop.md',
130
+ 'references/game-review-dimensions/depth-replayability.md',
131
+ 'references/game-review-dimensions/controls-input.md',
132
+ 'references/game-review-dimensions/ux-usability.md',
133
+ 'references/game-review-dimensions/first-time-user-experience.md',
134
+ 'references/game-review-dimensions/visuals-art-direction.md',
135
+ 'references/game-review-dimensions/audio-feedback.md',
136
+ 'references/game-review-dimensions/store-listing-metadata-accuracy.md',
137
+ 'references/game-review-dimensions/safety-age-rating-compliance.md',
138
+ 'references/game-review-dimensions/performance-stability.md',
139
+ ];
140
+ const PLAYDROP_PLUGIN_REFERENCE_FILES = [
141
+ ...PLAYDROP_PLUGIN_CREATION_REFERENCE_FILES,
142
+ ...PLAYDROP_PLUGIN_GAME_REVIEW_REFERENCE_FILES,
143
+ ];
108
144
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
109
145
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
110
146
  ['task', 'report'],
111
147
  ['task', 'report-catalogue'],
112
148
  ['task', 'upload'],
113
149
  ['task', 'done'],
150
+ ['task', 'submit-review'],
114
151
  ['task', 'fail'],
152
+ ['review', 'validate-result'],
153
+ ['review', 'compose-evidence'],
154
+ ['review', 'rating-card'],
115
155
  ['project', 'validate'],
116
156
  ['project', 'build'],
117
157
  ['project', 'dev'],
@@ -174,7 +214,7 @@ function normalizeAssignmentAgent(value) {
174
214
  return value;
175
215
  }
176
216
  function normalizeAssignmentKind(value, task) {
177
- if (value !== 'NEW_GAME' && value !== 'GAME_UPDATE') {
217
+ if (value !== 'NEW_GAME' && value !== 'GAME_UPDATE' && value !== 'GAME_REVIEW') {
178
218
  throw new Error('agent_task_assignment_kind_missing');
179
219
  }
180
220
  if (value !== task.kind) {
@@ -251,6 +291,7 @@ function resolveWorkerClaimTaskAssignment(claim) {
251
291
  },
252
292
  attachments: Array.isArray(inputs.attachments) ? inputs.attachments : [],
253
293
  baseSource: inputs.baseSource ?? null,
294
+ reviewTarget: inputs.reviewTarget ?? null,
254
295
  },
255
296
  output: {
256
297
  appId: typeof output.appId === 'number' && Number.isInteger(output.appId) && output.appId > 0 ? output.appId : null,
@@ -303,15 +344,31 @@ function buildWorkerTaskContext(claim, assignment) {
303
344
  if (!taskToken) {
304
345
  throw new Error('agent_task_context_token_missing');
305
346
  }
347
+ const attempt = Number(task.attempts);
348
+ if (!Number.isInteger(attempt) || attempt <= 0) {
349
+ throw new Error('agent_task_context_attempt_missing');
350
+ }
306
351
  const outputAppName = typeof assignment.output.appName === 'string' && assignment.output.appName.trim()
307
352
  ? assignment.output.appName.trim()
308
353
  : null;
354
+ const reviewTarget = assignment.inputs.reviewTarget && typeof assignment.inputs.reviewTarget === 'object'
355
+ ? assignment.inputs.reviewTarget
356
+ : null;
357
+ let reviewAppVersionId = null;
358
+ if (assignment.kind === 'GAME_REVIEW') {
359
+ const parsedReviewAppVersionId = Number(reviewTarget?.appVersionId);
360
+ if (!Number.isInteger(parsedReviewAppVersionId) || parsedReviewAppVersionId <= 0) {
361
+ throw new Error('agent_task_context_review_target_missing');
362
+ }
363
+ reviewAppVersionId = parsedReviewAppVersionId;
364
+ }
309
365
  const creatorRequest = assignment.inputs.request.prompt.trim();
310
366
  if (!creatorRequest) {
311
367
  throw new Error('agent_task_context_creator_request_missing');
312
368
  }
313
369
  return {
314
370
  taskId: task.id,
371
+ attempt,
315
372
  taskToken,
316
373
  kind: assignment.kind,
317
374
  creatorUsername,
@@ -319,6 +376,7 @@ function buildWorkerTaskContext(claim, assignment) {
319
376
  target: assignment.target,
320
377
  outputAppName,
321
378
  outputVersion,
379
+ reviewAppVersionId,
322
380
  };
323
381
  }
324
382
  function workerTaskContextPath(workspaceDir) {
@@ -339,6 +397,8 @@ async function stageWorkerTaskContext(input) {
339
397
  await (0, promises_1.writeFile)(node_path_1.default.join(input.workspaceDir, '.playdrop.json'), JSON.stringify({
340
398
  ownerUsername: input.taskContext.creatorUsername,
341
399
  env: input.env,
400
+ taskId: input.taskContext.taskId,
401
+ taskAttempt: input.taskContext.attempt,
342
402
  devPort: input.devPort,
343
403
  taskToken: input.taskContext.taskToken,
344
404
  }, null, 2), 'utf8');
@@ -361,25 +421,36 @@ function readTaskContextFile(startDir = node_process_1.default.cwd()) {
361
421
  const file = findWorkspaceTaskFile(startDir, 'task.json');
362
422
  const parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
363
423
  const taskId = Number(parsed.taskId);
424
+ const attempt = Number(parsed.attempt);
364
425
  const taskToken = typeof parsed.taskToken === 'string' ? parsed.taskToken.trim() : '';
365
426
  const creatorUsername = typeof parsed.creatorUsername === 'string' ? parsed.creatorUsername.trim() : '';
366
427
  const env = typeof parsed.env === 'string' ? parsed.env.trim() : '';
367
428
  const creatorRequest = typeof parsed.creatorRequest === 'string' ? parsed.creatorRequest.trim() : '';
368
429
  const outputVersion = typeof parsed.outputVersion === 'string' ? parsed.outputVersion.trim() : '';
430
+ let reviewAppVersionId = null;
431
+ if (parsed.reviewAppVersionId !== null && parsed.reviewAppVersionId !== undefined) {
432
+ const parsedReviewAppVersionId = Number(parsed.reviewAppVersionId);
433
+ if (Number.isInteger(parsedReviewAppVersionId) && parsedReviewAppVersionId > 0) {
434
+ reviewAppVersionId = parsedReviewAppVersionId;
435
+ }
436
+ }
369
437
  const devPort = typeof parsed.devPort === 'number' ? parsed.devPort : Number.parseInt(String(parsed.devPort ?? ''), 10);
370
438
  if (!Number.isInteger(taskId) || taskId <= 0
439
+ || !Number.isInteger(attempt) || attempt <= 0
371
440
  || !taskToken
372
- || (parsed.kind !== 'NEW_GAME' && parsed.kind !== 'GAME_UPDATE')
441
+ || (parsed.kind !== 'NEW_GAME' && parsed.kind !== 'GAME_UPDATE' && parsed.kind !== 'GAME_REVIEW')
373
442
  || (parsed.target !== 'FIRST_PARTY' && parsed.target !== 'PERSONAL')
374
443
  || !creatorUsername
375
444
  || !creatorRequest
376
445
  || !env
377
446
  || !outputVersion
447
+ || (parsed.kind === 'GAME_REVIEW' && reviewAppVersionId === null)
378
448
  || !Number.isInteger(devPort) || devPort <= 0 || devPort > 65535) {
379
449
  throw new Error(`task_context_invalid:${file}`);
380
450
  }
381
451
  return {
382
452
  taskId,
453
+ attempt,
383
454
  taskToken,
384
455
  kind: parsed.kind,
385
456
  creatorUsername,
@@ -387,6 +458,7 @@ function readTaskContextFile(startDir = node_process_1.default.cwd()) {
387
458
  target: parsed.target,
388
459
  outputAppName: typeof parsed.outputAppName === 'string' && parsed.outputAppName.trim() ? parsed.outputAppName.trim() : null,
389
460
  outputVersion,
461
+ reviewAppVersionId,
390
462
  devPort,
391
463
  env,
392
464
  };
@@ -588,8 +660,18 @@ async function stagePlaydropPluginReferences(input) {
588
660
  if (!hasPlaydropPluginReferences(pluginRoot)) {
589
661
  throw new Error(`playdrop_plugin_reference_missing: expected required PlayDrop plugin skills and scripts under ${pluginRoot}`);
590
662
  }
663
+ let referenceFiles;
664
+ if (input.kind === 'GAME_REVIEW') {
665
+ referenceFiles = PLAYDROP_PLUGIN_GAME_REVIEW_REFERENCE_FILES;
666
+ }
667
+ else if (input.kind === 'NEW_GAME' || input.kind === 'GAME_UPDATE') {
668
+ referenceFiles = PLAYDROP_PLUGIN_CREATION_REFERENCE_FILES;
669
+ }
670
+ else {
671
+ throw new Error(`unsupported_agent_task_kind:${input.kind}`);
672
+ }
591
673
  const staged = [];
592
- for (const relativePath of PLAYDROP_PLUGIN_REFERENCE_FILES) {
674
+ for (const relativePath of referenceFiles) {
593
675
  const destinationRelativePath = node_path_1.default.join(STAGED_PLAYDROP_PLUGIN_ROOT, relativePath);
594
676
  const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
595
677
  await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
@@ -1291,6 +1373,7 @@ async function runCodex(input) {
1291
1373
  env: (0, runtime_1.buildWorkerChildEnv)({
1292
1374
  binDir: input.binDir,
1293
1375
  taskId: input.taskId,
1376
+ attempt: input.attempt,
1294
1377
  envName: input.envName,
1295
1378
  eventDir: input.eventDir,
1296
1379
  devPort: input.devPort,
@@ -1317,6 +1400,7 @@ async function runClaude(input) {
1317
1400
  env: (0, runtime_1.buildWorkerChildEnv)({
1318
1401
  binDir: input.binDir,
1319
1402
  taskId: input.taskId,
1403
+ attempt: input.attempt,
1320
1404
  envName: input.envName,
1321
1405
  eventDir: input.eventDir,
1322
1406
  devPort: input.devPort,
@@ -1366,6 +1450,93 @@ function buildAgentRunResult(result) {
1366
1450
  tokensUsed: result.tokensUsed,
1367
1451
  };
1368
1452
  }
1453
+ function buildSupervisorFailureRunResult(message) {
1454
+ return {
1455
+ exitCode: null,
1456
+ signal: null,
1457
+ stdout: '',
1458
+ stderr: message,
1459
+ outputTail: message,
1460
+ timedOut: false,
1461
+ tokenUsage: {
1462
+ inputTokens: null,
1463
+ outputTokens: null,
1464
+ cacheCreationInputTokens: null,
1465
+ cacheReadInputTokens: null,
1466
+ totalTokens: null,
1467
+ rawProviderUsage: null,
1468
+ usageParseError: 'agent_not_started',
1469
+ },
1470
+ tokensUsed: null,
1471
+ };
1472
+ }
1473
+ function stripStagedPlaydropPluginPrefix(value) {
1474
+ const normalized = value.trim().replace(/\\/g, '/');
1475
+ const pluginPrefix = `${STAGED_PLAYDROP_PLUGIN_ROOT}/`;
1476
+ if (normalized.startsWith(pluginPrefix)) {
1477
+ return normalized.slice(pluginPrefix.length);
1478
+ }
1479
+ return normalized;
1480
+ }
1481
+ function normalizeTelemetrySkillPaths(stagedPaths) {
1482
+ return Array.from(new Set(stagedPaths.map(stripStagedPlaydropPluginPrefix).filter(Boolean))).sort();
1483
+ }
1484
+ function collectObservedPlaydropSkillPaths(output, availablePaths) {
1485
+ const observed = new Set();
1486
+ for (const availablePath of availablePaths) {
1487
+ if (output.includes(availablePath)
1488
+ || output.includes(`${STAGED_PLAYDROP_PLUGIN_ROOT}/${availablePath}`)) {
1489
+ observed.add(availablePath);
1490
+ }
1491
+ }
1492
+ return [...observed].sort();
1493
+ }
1494
+ function readProviderAccountLabel(target) {
1495
+ const label = node_process_1.default.env.PLAYDROP_WORKER_AGENT_ACCOUNT_LABEL?.trim() || null;
1496
+ if (target === 'FIRST_PARTY' && !label) {
1497
+ throw new Error('missing_playdrop_worker_agent_account_label');
1498
+ }
1499
+ return label;
1500
+ }
1501
+ function resolveTelemetryStatus(value) {
1502
+ if (value === 'QUEUED') {
1503
+ throw new Error('invalid_agent_task_run_status');
1504
+ }
1505
+ return value;
1506
+ }
1507
+ async function recordAgentRunTelemetry(input) {
1508
+ const observedSkillPaths = new Set(input.observedSkillPaths ?? []);
1509
+ for (const path of collectObservedPlaydropSkillPaths(`${input.result.stdout}\n${input.result.stderr}\n${input.result.outputTail}`, input.availableSkillPaths)) {
1510
+ observedSkillPaths.add(path);
1511
+ }
1512
+ await input.client.workerRecordAgentTaskRunTelemetry(input.task.id, {
1513
+ workerKey: input.workerKey,
1514
+ leaseToken: input.leaseToken,
1515
+ attempt: input.task.attempts,
1516
+ agent: input.assignment.agent,
1517
+ requestedModel: input.assignment.model,
1518
+ resolvedRuntimeModel: input.resolvedRuntimeModel,
1519
+ reasoningEffort: input.reasoningEffort,
1520
+ providerAccountLabel: input.providerAccountLabel,
1521
+ status: resolveTelemetryStatus(input.status),
1522
+ exitCode: input.result.exitCode,
1523
+ signal: input.result.signal,
1524
+ timedOut: input.result.timedOut,
1525
+ tokenUsage: {
1526
+ inputTokens: input.result.tokenUsage.inputTokens,
1527
+ outputTokens: input.result.tokenUsage.outputTokens,
1528
+ cacheCreationInputTokens: input.result.tokenUsage.cacheCreationInputTokens,
1529
+ cacheReadInputTokens: input.result.tokenUsage.cacheReadInputTokens,
1530
+ totalTokens: input.result.tokenUsage.totalTokens,
1531
+ rawProviderUsage: input.result.tokenUsage.rawProviderUsage,
1532
+ usageParseError: input.result.tokenUsage.usageParseError,
1533
+ },
1534
+ availableSkillPaths: [...input.availableSkillPaths],
1535
+ observedSkillPaths: [...observedSkillPaths].sort(),
1536
+ startedAt: input.startedAt.toISOString(),
1537
+ completedAt: input.completedAt.toISOString(),
1538
+ });
1539
+ }
1369
1540
  function providerNameForAgent(agent) {
1370
1541
  return agent === 'CLAUDE_CODE' ? 'claude' : 'codex';
1371
1542
  }
@@ -1634,11 +1805,49 @@ async function startWorker(options = {}) {
1634
1805
  let fenced = false;
1635
1806
  let retainWorkspace = false;
1636
1807
  let workspaceDir = null;
1808
+ let availableSkillPaths = [];
1809
+ const observedSkillPaths = new Set();
1810
+ let assignment = null;
1811
+ let providerAccountLabel = null;
1812
+ let resolvedRuntimeModel = task.model;
1813
+ let reasoningEffort = null;
1814
+ let agentStartedAt = null;
1815
+ let agentCompletedAt = null;
1816
+ let agentResult = null;
1817
+ let telemetryReported = false;
1818
+ const reportTelemetry = async (status, setupError) => {
1819
+ if (!assignment) {
1820
+ return;
1821
+ }
1822
+ const completedAt = agentCompletedAt ?? new Date();
1823
+ await recordAgentRunTelemetry({
1824
+ client,
1825
+ task,
1826
+ assignment,
1827
+ workerKey,
1828
+ leaseToken,
1829
+ result: agentResult ?? buildSupervisorFailureRunResult(setupError ?? 'worker_setup_failed'),
1830
+ status,
1831
+ resolvedRuntimeModel,
1832
+ reasoningEffort,
1833
+ providerAccountLabel,
1834
+ availableSkillPaths,
1835
+ observedSkillPaths: [...observedSkillPaths],
1836
+ startedAt: agentStartedAt ?? completedAt,
1837
+ completedAt,
1838
+ });
1839
+ telemetryReported = true;
1840
+ };
1637
1841
  try {
1638
- const assignment = resolveWorkerClaimTaskAssignment(claim);
1842
+ assignment = resolveWorkerClaimTaskAssignment(claim);
1639
1843
  if (!assignment) {
1640
1844
  throw new Error('agent_task_claim_missing_task_assignment');
1641
1845
  }
1846
+ providerAccountLabel = readProviderAccountLabel(assignment.target);
1847
+ const codexModel = assignment.agent === 'CODEX' ? resolveCodexModel(assignment.model) : null;
1848
+ const claudeModel = assignment.agent === 'CLAUDE_CODE' ? resolveClaudeModel(assignment.model) : null;
1849
+ resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.model;
1850
+ reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
1642
1851
  const taskContext = buildWorkerTaskContext(claim, assignment);
1643
1852
  workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
1644
1853
  await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
@@ -1669,6 +1878,20 @@ async function startWorker(options = {}) {
1669
1878
  eventDrainPromise = eventDrainPromise.then(() => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }), () => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }));
1670
1879
  return eventDrainPromise;
1671
1880
  };
1881
+ const appendObservedSkillPathsFromTranscript = (chunks) => {
1882
+ if (availableSkillPaths.length <= 0) {
1883
+ return;
1884
+ }
1885
+ for (const chunk of chunks) {
1886
+ for (const observedPath of collectObservedPlaydropSkillPaths(chunk.content, availableSkillPaths)) {
1887
+ observedSkillPaths.add(observedPath);
1888
+ }
1889
+ }
1890
+ };
1891
+ const appendTranscriptChunks = async (chunks) => {
1892
+ appendObservedSkillPathsFromTranscript(chunks);
1893
+ await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
1894
+ };
1672
1895
  await client.workerCreateAgentTaskEvent(task.id, {
1673
1896
  workerKey,
1674
1897
  leaseToken,
@@ -1677,7 +1900,7 @@ async function startWorker(options = {}) {
1677
1900
  message: 'Preparing worker workspace',
1678
1901
  pct: 2,
1679
1902
  });
1680
- await stagePlaydropPluginReferences({ workspaceDir, pluginRoot: playdropPluginRoot });
1903
+ availableSkillPaths = normalizeTelemetrySkillPaths(await stagePlaydropPluginReferences({ workspaceDir, pluginRoot: playdropPluginRoot, kind: task.kind }));
1681
1904
  await client.workerCreateAgentTaskEvent(task.id, {
1682
1905
  workerKey,
1683
1906
  leaseToken,
@@ -1747,13 +1970,22 @@ async function startWorker(options = {}) {
1747
1970
  throw new Error('agent_task_assignment_unexpected_base_source');
1748
1971
  }
1749
1972
  }
1973
+ else if (task.kind === 'GAME_REVIEW') {
1974
+ if (assignment.inputs.baseSource !== null) {
1975
+ throw new Error('agent_task_assignment_unexpected_base_source');
1976
+ }
1977
+ if (!assignment.inputs.reviewTarget) {
1978
+ throw new Error('agent_task_assignment_review_target_missing');
1979
+ }
1980
+ }
1750
1981
  else {
1751
1982
  throw new Error(`unsupported_agent_task_kind:${task.kind}`);
1752
1983
  }
1753
1984
  if (shuttingDown) {
1754
1985
  throw new Error('worker_shutdown');
1755
1986
  }
1756
- const agentResult = assignment.agent === 'CODEX'
1987
+ agentStartedAt = new Date();
1988
+ agentResult = assignment.agent === 'CODEX'
1757
1989
  ? await runCodex({
1758
1990
  workspaceDir,
1759
1991
  binDir,
@@ -1761,10 +1993,11 @@ async function startWorker(options = {}) {
1761
1993
  prompt,
1762
1994
  envName: env,
1763
1995
  taskId: task.id,
1996
+ attempt: taskContext.attempt,
1764
1997
  devPort,
1765
- codexModel: resolveCodexModel(assignment.model),
1998
+ codexModel: codexModel ?? resolveCodexModel(assignment.model),
1766
1999
  onTranscriptChunks: async (chunks) => {
1767
- await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
2000
+ await appendTranscriptChunks(chunks);
1768
2001
  },
1769
2002
  onChild: (controls) => {
1770
2003
  activeTerminators.set(task.id, controls.terminate);
@@ -1777,22 +2010,33 @@ async function startWorker(options = {}) {
1777
2010
  prompt,
1778
2011
  envName: env,
1779
2012
  taskId: task.id,
2013
+ attempt: taskContext.attempt,
1780
2014
  devPort,
1781
- claudeModel: resolveClaudeModel(assignment.model),
2015
+ claudeModel: claudeModel ?? resolveClaudeModel(assignment.model),
1782
2016
  playdropPluginRoot,
1783
2017
  onTranscriptChunks: async (chunks) => {
1784
- await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
2018
+ await appendTranscriptChunks(chunks);
1785
2019
  },
1786
2020
  onChild: (controls) => {
1787
2021
  activeTerminators.set(task.id, controls.terminate);
1788
2022
  },
1789
2023
  });
2024
+ agentCompletedAt = new Date();
1790
2025
  await queueEventDrain().catch((error) => {
1791
2026
  handleEventDrainFailure(error);
1792
2027
  });
1793
- const agentRunResult = buildAgentRunResult(agentResult);
2028
+ const completedAgentResult = agentResult;
2029
+ const agentRunResult = buildAgentRunResult(completedAgentResult);
1794
2030
  if (fenced) {
1795
- console.error(`Agent task ${task.id} was aborted by the server; cleaning up without reporting failure.`);
2031
+ const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
2032
+ if (refreshed && refreshed.task.status !== 'RUNNING' && refreshed.task.status !== 'QUEUED') {
2033
+ retainWorkspace = refreshed.task.status === 'FAILED';
2034
+ console.error(`Agent task ${task.id} was finalized by the server as ${refreshed.task.status}.`);
2035
+ await reportTelemetry(refreshed.task.status);
2036
+ }
2037
+ else {
2038
+ console.error(`Agent task ${task.id} was aborted by the server; cleaning up without reporting failure.`);
2039
+ }
1796
2040
  }
1797
2041
  else if (sessionExpired) {
1798
2042
  // Best effort: the session is already invalid, so this fail call
@@ -1811,12 +2055,14 @@ async function startWorker(options = {}) {
1811
2055
  error: 'worker_shutdown',
1812
2056
  result: agentRunResult,
1813
2057
  });
2058
+ await reportTelemetry('FAILED');
1814
2059
  }
1815
- else if (agentResult.timedOut || agentResult.exitCode !== 0 || agentResult.signal) {
2060
+ else if (completedAgentResult.timedOut || completedAgentResult.exitCode !== 0 || completedAgentResult.signal) {
1816
2061
  const refreshed = await fetchTaskDetail(client, target, task.id);
1817
2062
  if (refreshed.task.status !== 'RUNNING') {
1818
2063
  retainWorkspace = refreshed.task.status === 'FAILED';
1819
2064
  console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
2065
+ await reportTelemetry(refreshed.task.status);
1820
2066
  }
1821
2067
  else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
1822
2068
  retainWorkspace = true;
@@ -1828,10 +2074,11 @@ async function startWorker(options = {}) {
1828
2074
  phase: 'upload',
1829
2075
  message: 'Agent exited after upload before task done; manual completion repair required.',
1830
2076
  });
2077
+ await reportTelemetry('RUNNING');
1831
2078
  }
1832
2079
  else {
1833
2080
  retainWorkspace = true;
1834
- const failureCode = buildAgentFailureCode(assignment.agent, agentResult);
2081
+ const failureCode = buildAgentFailureCode(assignment.agent, completedAgentResult);
1835
2082
  await client.workerCreateAgentTaskEvent(task.id, {
1836
2083
  workerKey,
1837
2084
  leaseToken,
@@ -1847,6 +2094,7 @@ async function startWorker(options = {}) {
1847
2094
  error: failureCode,
1848
2095
  result: agentRunResult,
1849
2096
  });
2097
+ await reportTelemetry('FAILED');
1850
2098
  }
1851
2099
  }
1852
2100
  else {
@@ -1854,6 +2102,7 @@ async function startWorker(options = {}) {
1854
2102
  if (refreshed.task.status !== 'RUNNING') {
1855
2103
  retainWorkspace = refreshed.task.status === 'FAILED';
1856
2104
  console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
2105
+ await reportTelemetry(refreshed.task.status);
1857
2106
  }
1858
2107
  else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
1859
2108
  retainWorkspace = true;
@@ -1865,6 +2114,7 @@ async function startWorker(options = {}) {
1865
2114
  phase: 'upload',
1866
2115
  message: 'Agent exited after upload before task done; manual completion repair required.',
1867
2116
  });
2117
+ await reportTelemetry('RUNNING');
1868
2118
  }
1869
2119
  else {
1870
2120
  const tokenCap = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_TOKEN_CAP', runtime_1.DEFAULT_WORKER_TOKEN_CAP);
@@ -1880,8 +2130,9 @@ async function startWorker(options = {}) {
1880
2130
  workerKey,
1881
2131
  leaseToken,
1882
2132
  kind: 'progress',
1883
- message: `Token usage ${agentResult.tokensUsed} exceeded the worker token cap of ${tokenCap}.`,
2133
+ message: `Token usage ${completedAgentResult.tokensUsed} exceeded the worker token cap of ${tokenCap}.`,
1884
2134
  });
2135
+ await reportTelemetry('FAILED');
1885
2136
  throw error;
1886
2137
  }
1887
2138
  if (!tokenUsageKnown) {
@@ -1906,6 +2157,7 @@ async function startWorker(options = {}) {
1906
2157
  error: 'agent_exited_without_task_done',
1907
2158
  result: agentRunResult,
1908
2159
  });
2160
+ await reportTelemetry('FAILED');
1909
2161
  }
1910
2162
  }
1911
2163
  }
@@ -1921,12 +2173,24 @@ async function startWorker(options = {}) {
1921
2173
  console.error(`Agent task ${task.id} failed in the worker: ${message}`);
1922
2174
  }
1923
2175
  if (!fenced && !isAgentTaskNotRunningError(error)) {
1924
- await failTaskWithRetry(client, task.id, {
1925
- workerKey,
1926
- leaseToken,
1927
- error: normalizeWorkerFailureErrorCode(message),
1928
- result: { workerError: message },
1929
- });
2176
+ try {
2177
+ await failTaskWithRetry(client, task.id, {
2178
+ workerKey,
2179
+ leaseToken,
2180
+ error: normalizeWorkerFailureErrorCode(message),
2181
+ result: { workerError: message },
2182
+ });
2183
+ }
2184
+ finally {
2185
+ if (!telemetryReported) {
2186
+ try {
2187
+ await reportTelemetry('FAILED', message);
2188
+ }
2189
+ catch (telemetryError) {
2190
+ console.error(`Agent task telemetry failed: ${telemetryError instanceof Error ? telemetryError.message : String(telemetryError)}`);
2191
+ }
2192
+ }
2193
+ }
1930
2194
  }
1931
2195
  }
1932
2196
  finally {
@@ -2123,6 +2387,9 @@ async function resolveTaskCommandContext(command, optionsEnv, taskContext) {
2123
2387
  }
2124
2388
  async function uploadTask(options = {}) {
2125
2389
  const taskContext = readTaskContextFile();
2390
+ if (taskContext.kind === 'GAME_REVIEW') {
2391
+ throw new Error('task_upload_not_allowed_for_game_review');
2392
+ }
2126
2393
  const workspaceDir = resolveTaskWorkspaceDir();
2127
2394
  if ((0, node_fs_1.existsSync)(workerTaskUploadResultPath(workspaceDir))) {
2128
2395
  const existingResult = readTaskUploadResultFile(workspaceDir);
@@ -2169,6 +2436,9 @@ async function uploadTask(options = {}) {
2169
2436
  }
2170
2437
  async function completeTask(options) {
2171
2438
  const taskContext = readTaskContextFile();
2439
+ if (taskContext.kind === 'GAME_REVIEW') {
2440
+ throw new Error('task_done_not_allowed_for_game_review');
2441
+ }
2172
2442
  const ctx = await resolveTaskCommandContext('task done', options.env, taskContext);
2173
2443
  if (!ctx) {
2174
2444
  return;
@@ -2198,6 +2468,143 @@ async function completeTask(options) {
2198
2468
  });
2199
2469
  (0, output_1.printSuccess)('Task marked done.');
2200
2470
  }
2471
+ function normalizeReviewSubmitState(value) {
2472
+ const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
2473
+ if (normalized !== 'PASSED'
2474
+ && normalized !== 'GOOD'
2475
+ && normalized !== 'EXCELLENT'
2476
+ && normalized !== 'LOW_QUALITY'
2477
+ && normalized !== 'FAILED'
2478
+ && normalized !== 'ERROR') {
2479
+ throw new Error('invalid_review_state');
2480
+ }
2481
+ return normalized;
2482
+ }
2483
+ function readRequiredTextFile(filePath, code) {
2484
+ const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
2485
+ if (!normalizedPath) {
2486
+ throw new Error(code);
2487
+ }
2488
+ const resolved = node_path_1.default.resolve(normalizedPath);
2489
+ if (!(0, node_fs_1.existsSync)(resolved)) {
2490
+ throw new Error(code);
2491
+ }
2492
+ const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
2493
+ if (!content) {
2494
+ throw new Error(code);
2495
+ }
2496
+ return content;
2497
+ }
2498
+ function readOptionalTextFile(filePath) {
2499
+ const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
2500
+ if (!normalizedPath) {
2501
+ return null;
2502
+ }
2503
+ const resolved = node_path_1.default.resolve(normalizedPath);
2504
+ if (!(0, node_fs_1.existsSync)(resolved)) {
2505
+ throw new Error('invalid_creator_feedback_file');
2506
+ }
2507
+ const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
2508
+ return content || null;
2509
+ }
2510
+ function contentTypeForEvidenceFile(fileName) {
2511
+ const extension = node_path_1.default.extname(fileName).toLowerCase();
2512
+ if (extension === '.png')
2513
+ return 'image/png';
2514
+ if (extension === '.jpg' || extension === '.jpeg')
2515
+ return 'image/jpeg';
2516
+ if (extension === '.webp')
2517
+ return 'image/webp';
2518
+ return 'application/octet-stream';
2519
+ }
2520
+ function readReviewEvidenceFiles(evidenceDir) {
2521
+ const normalizedDir = typeof evidenceDir === 'string' ? evidenceDir.trim() : '';
2522
+ if (!normalizedDir) {
2523
+ throw new Error('invalid_review_evidence_dir');
2524
+ }
2525
+ const resolvedDir = node_path_1.default.resolve(normalizedDir);
2526
+ if (!(0, node_fs_1.existsSync)(resolvedDir)) {
2527
+ throw new Error('invalid_review_evidence_dir');
2528
+ }
2529
+ const existingFiles = new Set((0, node_fs_1.readdirSync)(resolvedDir, { withFileTypes: true })
2530
+ .filter((entry) => entry.isFile())
2531
+ .map((entry) => entry.name));
2532
+ const files = review_1.REQUIRED_REVIEW_EVIDENCE_FILES.filter((fileName) => existingFiles.has(fileName));
2533
+ if (files.length !== review_1.REQUIRED_REVIEW_EVIDENCE_FILES.length) {
2534
+ const missing = review_1.REQUIRED_REVIEW_EVIDENCE_FILES.filter((fileName) => !existingFiles.has(fileName));
2535
+ throw new Error(`missing_review_evidence:${missing.join(',')}`);
2536
+ }
2537
+ return files.map((fileName) => {
2538
+ const filePath = node_path_1.default.join(resolvedDir, fileName);
2539
+ const buffer = (0, node_fs_1.readFileSync)(filePath);
2540
+ if (buffer.length <= 0 || buffer.length > 10 * 1024 * 1024) {
2541
+ throw new Error('invalid_review_evidence_size');
2542
+ }
2543
+ return {
2544
+ name: fileName,
2545
+ contentType: contentTypeForEvidenceFile(fileName),
2546
+ contentBase64: buffer.toString('base64'),
2547
+ };
2548
+ });
2549
+ }
2550
+ function resolveReviewFilePath(filePath, code) {
2551
+ const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
2552
+ if (!normalizedPath) {
2553
+ throw new Error(code);
2554
+ }
2555
+ const resolved = node_path_1.default.resolve(normalizedPath);
2556
+ if (!(0, node_fs_1.existsSync)(resolved)) {
2557
+ throw new Error(code);
2558
+ }
2559
+ return resolved;
2560
+ }
2561
+ function resolveReviewEvidenceDir(evidenceDir) {
2562
+ const normalizedDir = typeof evidenceDir === 'string' ? evidenceDir.trim() : '';
2563
+ if (!normalizedDir) {
2564
+ throw new Error('invalid_review_evidence_dir');
2565
+ }
2566
+ const resolved = node_path_1.default.resolve(normalizedDir);
2567
+ if (!(0, node_fs_1.existsSync)(resolved)) {
2568
+ throw new Error('invalid_review_evidence_dir');
2569
+ }
2570
+ return resolved;
2571
+ }
2572
+ async function submitReviewTask(options) {
2573
+ const taskContext = readTaskContextFile();
2574
+ if (taskContext.kind !== 'GAME_REVIEW') {
2575
+ throw new Error('task_submit_review_requires_game_review');
2576
+ }
2577
+ const state = normalizeReviewSubmitState(options.state);
2578
+ const reviewMessageFile = resolveReviewFilePath(options.messageFile, 'invalid_review_message_file');
2579
+ const creatorFeedbackFile = state === 'ERROR'
2580
+ ? null
2581
+ : resolveReviewFilePath(options.creatorFeedbackFile, 'invalid_creator_feedback_file');
2582
+ const evidenceDir = resolveReviewEvidenceDir(options.evidenceDir);
2583
+ const reviewMessage = readRequiredTextFile(reviewMessageFile, 'invalid_review_message_file');
2584
+ const creatorFeedback = state === 'ERROR'
2585
+ ? null
2586
+ : readOptionalTextFile(creatorFeedbackFile ?? undefined);
2587
+ await (0, review_1.validateGameReviewResult)({
2588
+ reviewState: state,
2589
+ reviewMessage,
2590
+ creatorFeedback: creatorFeedback ?? '',
2591
+ evidenceDir,
2592
+ });
2593
+ const evidenceFiles = readReviewEvidenceFiles(evidenceDir);
2594
+ const ctx = await resolveTaskCommandContext('task submit-review', options.env, taskContext);
2595
+ if (!ctx) {
2596
+ throw new Error('task_submit_review_auth_required');
2597
+ }
2598
+ await ctx.client.workerSubmitAgentTaskReview(taskContext.taskId, {
2599
+ taskToken: taskContext.taskToken,
2600
+ appVersionId: taskContext.reviewAppVersionId ?? 0,
2601
+ reviewState: state,
2602
+ reviewMessage,
2603
+ creatorFeedback,
2604
+ evidenceFiles,
2605
+ });
2606
+ (0, output_1.printSuccess)('Review submitted.');
2607
+ }
2201
2608
  async function failTask(options) {
2202
2609
  const message = options.message?.trim();
2203
2610
  if (!message) {