@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.
- package/config/client-meta.json +2 -1
- package/dist/apiClient.d.ts +8 -0
- package/dist/apiClient.js +29 -1
- package/dist/captureRuntime.d.ts +13 -0
- package/dist/captureRuntime.js +21 -0
- package/dist/commandContext.js +21 -3
- package/dist/commands/captureRemote.d.ts +2 -0
- package/dist/commands/captureRemote.js +90 -0
- package/dist/commands/review.d.ts +46 -0
- package/dist/commands/review.js +353 -0
- package/dist/commands/worker/runtime.d.ts +12 -0
- package/dist/commands/worker/runtime.js +79 -35
- package/dist/commands/worker.d.ts +17 -3
- package/dist/commands/worker.js +431 -24
- package/dist/index.js +45 -0
- package/dist/workspaceAuth.d.ts +2 -0
- package/dist/workspaceAuth.js +6 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +6 -2
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +3 -2
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.js +6 -3
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +4 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +36 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -2
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +21 -6
- package/node_modules/@playdrop/config/client-meta.json +2 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +163 -3
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +11 -1
- package/package.json +1 -1
package/dist/commands/worker.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2028
|
+
const completedAgentResult = agentResult;
|
|
2029
|
+
const agentRunResult = buildAgentRunResult(completedAgentResult);
|
|
1794
2030
|
if (fenced) {
|
|
1795
|
-
|
|
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 (
|
|
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,
|
|
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 ${
|
|
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
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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) {
|