@playdrop/playdrop-cli 0.12.1 → 0.12.2

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.
@@ -45,6 +45,8 @@ exports.parseLaunchAgentProgramArguments = parseLaunchAgentProgramArguments;
45
45
  exports.assertLaunchAgentPlistCompatible = assertLaunchAgentPlistCompatible;
46
46
  exports.showWorkerStatus = showWorkerStatus;
47
47
  exports.setupWorker = setupWorker;
48
+ exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
49
+ exports.enforceInstrumentBrowserHygiene = enforceInstrumentBrowserHygiene;
48
50
  exports.startWorker = startWorker;
49
51
  exports.reportTask = reportTask;
50
52
  exports.reportCatalogueTask = reportCatalogueTask;
@@ -54,9 +56,12 @@ exports.completeTask = completeTask;
54
56
  exports.readReviewEvidenceFiles = readReviewEvidenceFiles;
55
57
  exports.submitReviewTask = submitReviewTask;
56
58
  exports.submitEvalTask = submitEvalTask;
59
+ exports.saveTaskEvidence = saveTaskEvidence;
60
+ exports.instrumentErrorTask = instrumentErrorTask;
57
61
  exports.failTask = failTask;
58
62
  const types_1 = require("@playdrop/types");
59
63
  const fflate_1 = require("fflate");
64
+ const node_child_process_1 = require("node:child_process");
60
65
  const node_crypto_1 = require("node:crypto");
61
66
  const node_fs_1 = require("node:fs");
62
67
  const promises_1 = require("node:fs/promises");
@@ -64,6 +69,7 @@ const node_module_1 = require("node:module");
64
69
  const node_os_1 = __importDefault(require("node:os"));
65
70
  const node_path_1 = __importDefault(require("node:path"));
66
71
  const node_process_1 = __importDefault(require("node:process"));
72
+ const node_util_1 = require("node:util");
67
73
  const clientInfo_1 = require("../clientInfo");
68
74
  const commandContext_1 = require("../commandContext");
69
75
  const config_1 = require("../config");
@@ -74,6 +80,7 @@ const agents_1 = require("./agents");
74
80
  const review_1 = require("./review");
75
81
  const upload_1 = require("./upload");
76
82
  const runtime_1 = require("./worker/runtime");
83
+ const instrument_evidence_1 = require("./worker/instrument-evidence");
77
84
  var runtime_2 = require("./worker/runtime");
78
85
  Object.defineProperty(exports, "assertWorkerTokenUsageWithinCap", { enumerable: true, get: function () { return runtime_2.assertWorkerTokenUsageWithinCap; } });
79
86
  Object.defineProperty(exports, "buildClaudeExecArgs", { enumerable: true, get: function () { return runtime_2.buildClaudeExecArgs; } });
@@ -92,6 +99,7 @@ Object.defineProperty(exports, "readEnvBoolean", { enumerable: true, get: functi
92
99
  Object.defineProperty(exports, "runLoggedProcess", { enumerable: true, get: function () { return runtime_2.runLoggedProcess; } });
93
100
  const DEFAULT_POLL_INTERVAL_MS = 1000;
94
101
  const HEARTBEAT_INTERVAL_MS = 10000;
102
+ const INSTRUMENT_HEARTBEAT_INTERVAL_MS = 1000;
95
103
  const TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT = 6;
96
104
  const DEFAULT_WORKER_MAX_PARALLEL_TASKS = 5;
97
105
  const DEFAULT_WORKER_DEV_PORT_BASE = 8900;
@@ -109,6 +117,7 @@ const STAGED_PLAYDROP_PLUGIN_ROOT = '.playdrop/plugin';
109
117
  const PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH = 'playdrop-worker-staging.json';
110
118
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES = 1000;
111
119
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_UNCOMPRESSED_BYTES = 20 * 1024 * 1024;
120
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
112
121
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
113
122
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
114
123
  ['task', 'report'],
@@ -118,10 +127,10 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
118
127
  ['task', 'done'],
119
128
  ['task', 'submit-review'],
120
129
  ['task', 'submit-eval'],
130
+ ['task', 'evidence'],
131
+ ['task', 'instrument-error'],
121
132
  ['task', 'fail'],
122
133
  ['review', 'validate-result'],
123
- ['review', 'list-windows'],
124
- ['review', 'screenshot'],
125
134
  ['review', 'compose-evidence'],
126
135
  ['review', 'rating-card'],
127
136
  ['project', 'validate'],
@@ -1838,7 +1847,10 @@ async function runCodex(input) {
1838
1847
  }
1839
1848
  async function runClaude(input) {
1840
1849
  const denyReadRoots = resolveClaudeDenyReadRoots(input.workspaceDir);
1841
- return await (0, runtime_1.runLoggedProcess)({
1850
+ const screenshotCollector = input.enableChrome
1851
+ ? (0, instrument_evidence_1.createWorkerInstrumentScreenshotCollector)(input.workspaceDir)
1852
+ : null;
1853
+ const result = await (0, runtime_1.runLoggedProcess)({
1842
1854
  command: 'claude',
1843
1855
  args: (0, runtime_1.buildClaudeExecArgs)({
1844
1856
  ...input.claudeModel,
@@ -1860,9 +1872,14 @@ async function runClaude(input) {
1860
1872
  timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
1861
1873
  maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
1862
1874
  onTranscriptChunks: input.onTranscriptChunks,
1875
+ ...(screenshotCollector
1876
+ ? { onStdoutText: (text) => screenshotCollector.push(text) > 0 }
1877
+ : {}),
1863
1878
  transcriptFlushIntervalMs: runtime_1.DEFAULT_TRANSCRIPT_FLUSH_INTERVAL_MS,
1864
1879
  onChild: input.onChild,
1865
1880
  });
1881
+ screenshotCollector?.finish();
1882
+ return result;
1866
1883
  }
1867
1884
  function isPathInside(parent, child) {
1868
1885
  const relative = node_path_1.default.relative(parent, child);
@@ -3077,6 +3094,138 @@ async function fetchTaskDetail(client, target, taskId) {
3077
3094
  }
3078
3095
  return await client.getAgentTask(taskId);
3079
3096
  }
3097
+ function instrumentTaskChromeTitleMatchers(assignment) {
3098
+ const targets = assignment.kind === 'GAME_REVIEW'
3099
+ ? [assignment.inputs.reviewTarget]
3100
+ : assignment.kind === 'GAME_EVAL'
3101
+ ? assignment.inputs.evalTarget?.targets ?? []
3102
+ : [];
3103
+ const matchers = new Set();
3104
+ for (const target of targets) {
3105
+ if (!target)
3106
+ continue;
3107
+ for (const value of [target.appDisplayName, target.appName]) {
3108
+ const normalized = typeof value === 'string' ? value.trim() : '';
3109
+ if (normalized.length >= 3) {
3110
+ matchers.add(normalized);
3111
+ }
3112
+ }
3113
+ }
3114
+ return [...matchers].sort((left, right) => right.length - left.length);
3115
+ }
3116
+ const INSTRUMENT_BROWSER_HYGIENE_APPLESCRIPT = String.raw `
3117
+ on run matchers
3118
+ tell application "Google Chrome"
3119
+ repeat with windowIndex from (count of windows) to 1 by -1
3120
+ try
3121
+ set currentWindow to window windowIndex
3122
+ repeat with tabIndex from (count of tabs of currentWindow) to 1 by -1
3123
+ set currentTab to tab tabIndex of currentWindow
3124
+ set currentTitle to title of currentTab
3125
+ set shouldClose to false
3126
+ repeat with matcher in matchers
3127
+ if currentTitle contains (matcher as text) then
3128
+ set shouldClose to true
3129
+ exit repeat
3130
+ end if
3131
+ end repeat
3132
+ if shouldClose then close currentTab
3133
+ end repeat
3134
+ end try
3135
+ end repeat
3136
+
3137
+ set blankWindowCount to 0
3138
+ repeat with currentWindow in windows
3139
+ try
3140
+ if (count of tabs of currentWindow) is 1 then
3141
+ set currentUrl to URL of active tab of currentWindow
3142
+ if currentUrl is "about:blank" or currentUrl starts with "chrome://newtab" then
3143
+ set blankWindowCount to blankWindowCount + 1
3144
+ end if
3145
+ end if
3146
+ end try
3147
+ end repeat
3148
+ if blankWindowCount is 0 then
3149
+ set blankWindow to make new window
3150
+ set URL of active tab of blankWindow to "about:blank"
3151
+ delay 1
3152
+ end if
3153
+
3154
+ set keptBlankWindow to false
3155
+ repeat with windowIndex from (count of windows) to 1 by -1
3156
+ try
3157
+ set currentWindow to window windowIndex
3158
+ if (count of tabs of currentWindow) is 1 then
3159
+ set currentUrl to URL of active tab of currentWindow
3160
+ if currentUrl is "about:blank" or currentUrl starts with "chrome://newtab" then
3161
+ if keptBlankWindow then
3162
+ close currentWindow
3163
+ else
3164
+ set keptBlankWindow to true
3165
+ end if
3166
+ end if
3167
+ end if
3168
+ end try
3169
+ end repeat
3170
+
3171
+ set remainingCount to 0
3172
+ set blankWindowCount to 0
3173
+ repeat with currentWindow in windows
3174
+ try
3175
+ repeat with currentTab in tabs of currentWindow
3176
+ set currentTitle to title of currentTab
3177
+ repeat with matcher in matchers
3178
+ if currentTitle contains (matcher as text) then
3179
+ set remainingCount to remainingCount + 1
3180
+ exit repeat
3181
+ end if
3182
+ end repeat
3183
+ end repeat
3184
+ if (count of tabs of currentWindow) is 1 then
3185
+ set currentUrl to URL of active tab of currentWindow
3186
+ if currentUrl is "about:blank" or currentUrl starts with "chrome://newtab" then
3187
+ set blankWindowCount to blankWindowCount + 1
3188
+ end if
3189
+ end if
3190
+ end try
3191
+ end repeat
3192
+ end tell
3193
+ return "remaining=" & remainingCount & ";blank=" & blankWindowCount
3194
+ end run
3195
+ `;
3196
+ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
3197
+ if (assignment.kind !== 'GAME_REVIEW' && assignment.kind !== 'GAME_EVAL') {
3198
+ return { remainingTaskTabs: 0, blankWindows: 0 };
3199
+ }
3200
+ if ((options.platform ?? node_process_1.default.platform) !== 'darwin') {
3201
+ throw new Error('worker_browser_hygiene_requires_macos');
3202
+ }
3203
+ const matchers = instrumentTaskChromeTitleMatchers(assignment);
3204
+ if (matchers.length === 0) {
3205
+ throw new Error('worker_browser_hygiene_missing_title_matchers');
3206
+ }
3207
+ const runner = options.runner ?? (async (command, args) => {
3208
+ const result = await execFileAsync(command, args, { timeout: 15000, maxBuffer: 1024 * 1024 });
3209
+ return { stdout: result.stdout, stderr: result.stderr };
3210
+ });
3211
+ let result;
3212
+ try {
3213
+ result = await runner('osascript', ['-e', INSTRUMENT_BROWSER_HYGIENE_APPLESCRIPT, ...matchers]);
3214
+ }
3215
+ catch (error) {
3216
+ throw new Error(`worker_browser_hygiene_automation_failed:${error instanceof Error ? error.message : String(error)}`);
3217
+ }
3218
+ const match = /remaining=(\d+);blank=(\d+)/.exec(result.stdout.trim());
3219
+ if (!match) {
3220
+ throw new Error(`worker_browser_hygiene_invalid_result:${result.stdout.trim() || result.stderr.trim() || 'empty'}`);
3221
+ }
3222
+ const remainingTaskTabs = Number(match[1]);
3223
+ const blankWindows = Number(match[2]);
3224
+ if (remainingTaskTabs !== 0 || blankWindows !== 1) {
3225
+ throw new Error(`worker_browser_hygiene_failed:remaining=${remainingTaskTabs}:blank=${blankWindows}`);
3226
+ }
3227
+ return { remainingTaskTabs, blankWindows };
3228
+ }
3080
3229
  function throwWorkerRuntimeError(error) {
3081
3230
  throw error;
3082
3231
  }
@@ -3443,7 +3592,9 @@ async function startWorker(options = {}) {
3443
3592
  }
3444
3593
  console.error(`Agent task heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
3445
3594
  });
3446
- }, HEARTBEAT_INTERVAL_MS);
3595
+ }, task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL'
3596
+ ? INSTRUMENT_HEARTBEAT_INTERVAL_MS
3597
+ : HEARTBEAT_INTERVAL_MS);
3447
3598
  eventDrainTimer = setInterval(() => {
3448
3599
  queueEventDrain().catch((error) => {
3449
3600
  handleEventDrainFailure(error);
@@ -3620,7 +3771,7 @@ async function startWorker(options = {}) {
3620
3771
  await client.workerCreateAgentTaskEvent(task.id, {
3621
3772
  workerKey,
3622
3773
  leaseToken,
3623
- kind: 'system',
3774
+ kind: 'progress',
3624
3775
  phase: 'failed',
3625
3776
  message: describeAgentFailureForEvent(failureCode),
3626
3777
  pct: null,
@@ -3738,6 +3889,20 @@ async function startWorker(options = {}) {
3738
3889
  if (eventDrainTimer) {
3739
3890
  clearInterval(eventDrainTimer);
3740
3891
  }
3892
+ if (assignment
3893
+ && agentStartedAt
3894
+ && (assignment.kind === 'GAME_REVIEW' || assignment.kind === 'GAME_EVAL')) {
3895
+ try {
3896
+ const hygiene = await enforceInstrumentBrowserHygiene(assignment);
3897
+ console.log(`Agent task ${task.id} browser hygiene passed: ${hygiene.remainingTaskTabs} task tabs, ${hygiene.blankWindows} blank window(s).`);
3898
+ }
3899
+ catch (hygieneError) {
3900
+ const error = hygieneError instanceof Error ? hygieneError : new Error(String(hygieneError));
3901
+ console.error(`Agent task ${task.id} browser hygiene failed: ${error.message}`);
3902
+ retainWorkspace = true;
3903
+ recordFatalTaskError(error);
3904
+ }
3905
+ }
3741
3906
  activeTerminators.delete(task.id);
3742
3907
  if (retainWorkspace && (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_RETAIN_FAILED_WORKSPACE', true)) {
3743
3908
  console.error(`Retained failed task workspace for debugging: ${workspaceDir ?? '(not created)'}`);
@@ -4136,7 +4301,7 @@ function contentTypeForEvidenceFile(fileName) {
4136
4301
  return 'image/webp';
4137
4302
  return 'application/octet-stream';
4138
4303
  }
4139
- function readReviewEvidenceFiles(evidenceDir) {
4304
+ function readReviewEvidenceFiles(evidenceDir, workspaceDir = resolveTaskWorkspaceDir()) {
4140
4305
  const normalizedDir = typeof evidenceDir === 'string' ? evidenceDir.trim() : '';
4141
4306
  if (!normalizedDir) {
4142
4307
  throw new Error('invalid_review_evidence_dir');
@@ -4145,6 +4310,7 @@ function readReviewEvidenceFiles(evidenceDir) {
4145
4310
  if (!(0, node_fs_1.existsSync)(resolvedDir)) {
4146
4311
  throw new Error('invalid_review_evidence_dir');
4147
4312
  }
4313
+ (0, instrument_evidence_1.readAndValidateInstrumentEvidenceManifest)({ workspaceDir, evidenceDir: resolvedDir });
4148
4314
  const existingFiles = new Set((0, node_fs_1.readdirSync)(resolvedDir, { withFileTypes: true })
4149
4315
  .filter((entry) => entry.isFile())
4150
4316
  .map((entry) => entry.name));
@@ -4209,7 +4375,9 @@ async function submitReviewTask(options) {
4209
4375
  creatorFeedback: creatorFeedback ?? '',
4210
4376
  evidenceDir,
4211
4377
  });
4212
- const evidenceFiles = readReviewEvidenceFiles(evidenceDir);
4378
+ const workspaceDir = resolveTaskWorkspaceDir();
4379
+ const instrumentEvidence = (0, instrument_evidence_1.readAndValidateInstrumentEvidenceManifest)({ workspaceDir, evidenceDir });
4380
+ const evidenceFiles = readReviewEvidenceFiles(evidenceDir, workspaceDir);
4213
4381
  const ctx = await resolveTaskCommandContext('task submit-review', options.env, taskContext);
4214
4382
  if (!ctx) {
4215
4383
  throw new Error('task_submit_review_auth_required');
@@ -4231,8 +4399,9 @@ async function submitReviewTask(options) {
4231
4399
  creatorFeedback,
4232
4400
  ...(nextSteps !== undefined ? { nextSteps } : {}),
4233
4401
  evidenceFiles,
4402
+ instrumentEvidence,
4234
4403
  });
4235
- (0, output_1.printSuccess)('Review submitted. Send one short final status sentence now, then stop.');
4404
+ (0, output_1.printSuccess)('Review submitted. This was the final operation; send one short final status sentence and stop immediately.');
4236
4405
  }
4237
4406
  async function submitEvalTask(options) {
4238
4407
  const taskContext = readTaskContextFile();
@@ -4247,6 +4416,37 @@ async function submitEvalTask(options) {
4247
4416
  catch (error) {
4248
4417
  throw new Error(`invalid_eval_result_json:${error instanceof Error ? error.message : String(error)}`);
4249
4418
  }
4419
+ const evidenceDirs = Array.isArray(options.evidenceDir)
4420
+ ? options.evidenceDir.map((entry) => entry.trim()).filter(Boolean)
4421
+ : [];
4422
+ if (evidenceDirs.length <= 0) {
4423
+ throw new Error('task_submit_eval_evidence_required');
4424
+ }
4425
+ const workspaceDir = resolveTaskWorkspaceDir();
4426
+ const instrumentEvidence = [];
4427
+ const evidenceFiles = [];
4428
+ const groupIds = new Set();
4429
+ for (const evidenceDir of evidenceDirs) {
4430
+ const manifest = (0, instrument_evidence_1.readAndValidateInstrumentEvidenceManifest)({ workspaceDir, evidenceDir });
4431
+ if (groupIds.has(manifest.groupId)) {
4432
+ throw new Error(`duplicate_instrument_evidence_group:${manifest.groupId}`);
4433
+ }
4434
+ groupIds.add(manifest.groupId);
4435
+ instrumentEvidence.push(manifest);
4436
+ for (const name of ['first-frame', 'core', 'win', 'loss']) {
4437
+ const capture = manifest.captures[name];
4438
+ if (!capture) {
4439
+ throw new Error(`missing_instrument_evidence_capture:${manifest.groupId}:${name}`);
4440
+ }
4441
+ const filePath = node_path_1.default.join(node_path_1.default.resolve(evidenceDir), capture.fileName);
4442
+ evidenceFiles.push({
4443
+ groupId: manifest.groupId,
4444
+ name: capture.fileName,
4445
+ contentType: contentTypeForEvidenceFile(capture.fileName),
4446
+ contentBase64: (0, node_fs_1.readFileSync)(filePath).toString('base64'),
4447
+ });
4448
+ }
4449
+ }
4250
4450
  const ctx = await resolveTaskCommandContext('task submit-eval', options.env, taskContext);
4251
4451
  if (!ctx) {
4252
4452
  throw new Error('task_submit_eval_auth_required');
@@ -4254,8 +4454,61 @@ async function submitEvalTask(options) {
4254
4454
  await ctx.client.workerSubmitAgentTaskEval(taskContext.taskId, {
4255
4455
  taskToken: taskContext.taskToken,
4256
4456
  result: result,
4457
+ instrumentEvidence,
4458
+ evidenceFiles,
4459
+ });
4460
+ (0, output_1.printSuccess)('Eval submitted. This was the final operation; send one short final status sentence and stop immediately.');
4461
+ }
4462
+ async function saveTaskEvidence(options) {
4463
+ const taskContext = readTaskContextFile();
4464
+ if (taskContext.kind !== 'GAME_REVIEW' && taskContext.kind !== 'GAME_EVAL') {
4465
+ throw new Error('task_evidence_requires_instrument_task');
4466
+ }
4467
+ const evidenceDir = options.evidenceDir?.trim();
4468
+ const name = options.name?.trim();
4469
+ const screenshotId = options.screenshotId?.trim();
4470
+ const groupId = options.group?.trim() || (taskContext.kind === 'GAME_REVIEW' ? 'review' : '');
4471
+ if (!evidenceDir || !name || !screenshotId || !groupId) {
4472
+ throw new Error('task_evidence_options_required');
4473
+ }
4474
+ const capture = await (0, instrument_evidence_1.saveInstrumentEvidenceCapture)({
4475
+ workspaceDir: resolveTaskWorkspaceDir(),
4476
+ evidenceDir,
4477
+ groupId,
4478
+ name,
4479
+ screenshotId,
4480
+ });
4481
+ (0, output_1.printSuccess)(`Saved ${capture.name} evidence from Chrome screenshot ${capture.screenshotId} on controlled tab ${capture.tabId} to ${capture.fileName}.`);
4482
+ }
4483
+ const INSTRUMENT_ERROR_REASONS = new Set([
4484
+ 'auth_state',
4485
+ 'browser_control',
4486
+ 'renderer',
4487
+ 'screenshot',
4488
+ ]);
4489
+ async function instrumentErrorTask(options) {
4490
+ const reason = options.reason?.trim();
4491
+ if (!reason || !INSTRUMENT_ERROR_REASONS.has(reason)) {
4492
+ throw new Error(`invalid_instrument_error_reason:${reason ?? ''}`);
4493
+ }
4494
+ const taskContext = readTaskContextFile();
4495
+ if (taskContext.kind !== 'GAME_REVIEW' && taskContext.kind !== 'GAME_EVAL') {
4496
+ throw new Error('task_instrument_error_requires_instrument_task');
4497
+ }
4498
+ const ctx = await resolveTaskCommandContext('task instrument-error', options.env, taskContext);
4499
+ if (!ctx) {
4500
+ throw new Error('task_instrument_error_auth_required');
4501
+ }
4502
+ await ctx.client.workerFailAgentTask(taskContext.taskId, {
4503
+ taskToken: taskContext.taskToken,
4504
+ error: `instrument_error:${reason}`,
4505
+ result: {
4506
+ outcome: 'instrument_error',
4507
+ reason,
4508
+ reportedBy: 'agent',
4509
+ },
4257
4510
  });
4258
- (0, output_1.printSuccess)('Eval submitted. Send one short final status sentence now, then stop.');
4511
+ (0, output_1.printSuccess)(`Instrument error submitted (${reason}). This was the final operation; stop immediately.`);
4259
4512
  }
4260
4513
  async function failTask(options) {
4261
4514
  const message = options.message?.trim();
@@ -4274,5 +4527,5 @@ async function failTask(options) {
4274
4527
  error: message,
4275
4528
  result: { failedBy: 'agent' },
4276
4529
  });
4277
- (0, output_1.printSuccess)('Task marked failed. Send one short final status sentence now, then stop.');
4530
+ (0, output_1.printSuccess)('Task marked failed. This was the final operation; send one short final status sentence and stop immediately.');
4278
4531
  }
package/dist/index.js CHANGED
@@ -276,10 +276,29 @@ task
276
276
  .command('submit-eval')
277
277
  .description('Submit the active game eval task result')
278
278
  .requiredOption('--result-file <path>', 'Strict GAME_EVAL JSON result file')
279
+ .requiredOption('--evidence-dir <paths...>', 'One instrument evidence directory per eval target')
279
280
  .option('--env <env>', 'Environment override')
280
281
  .action(async (opts) => {
281
282
  await (0, worker_1.submitEvalTask)(opts);
282
283
  });
284
+ task
285
+ .command('evidence')
286
+ .description('Persist a Claude Chrome screenshot as task evidence')
287
+ .requiredOption('--name <name>', 'Evidence state: first-frame, core, win, or loss')
288
+ .requiredOption('--screenshot-id <id>', 'Claude Chrome screenshot id')
289
+ .requiredOption('--evidence-dir <path>', 'Evidence output directory')
290
+ .option('--group <id>', 'Evidence group id; required for GAME_EVAL')
291
+ .action(async (opts) => {
292
+ await (0, worker_1.saveTaskEvidence)(opts);
293
+ });
294
+ task
295
+ .command('instrument-error')
296
+ .description('Terminate a review or eval task with a structured instrument error')
297
+ .requiredOption('--reason <reason>', 'auth_state, browser_control, renderer, or screenshot')
298
+ .option('--env <env>', 'Environment override')
299
+ .action(async (opts) => {
300
+ await (0, worker_1.instrumentErrorTask)(opts);
301
+ });
283
302
  task
284
303
  .command('fail')
285
304
  .description('Fail the active agent task')
@@ -289,23 +308,6 @@ task
289
308
  await (0, worker_1.failTask)(opts);
290
309
  });
291
310
  const review = program.command('review').description('Review helper commands for PlayDrop worker tasks');
292
- review
293
- .command('list-windows')
294
- .description('List native capture windows for review evidence')
295
- .requiredOption('--pid <pid>', 'Browser process id')
296
- .action(async (opts) => {
297
- await (0, review_1.listReviewCaptureWindows)(opts);
298
- });
299
- review
300
- .command('screenshot')
301
- .description('Capture a native screenshot for review evidence')
302
- .requiredOption('--pid <pid>', 'Browser process id')
303
- .option('--window-id <id>', 'Native capture window id')
304
- .requiredOption('--out <path>', 'Output PNG path')
305
- .option('--metadata <path>', 'Output metadata JSON path')
306
- .action(async (opts) => {
307
- await (0, review_1.captureReviewScreenshot)(opts);
308
- });
309
311
  review
310
312
  .command('validate-result')
311
313
  .description('Validate a canonical game review result')
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.12.1",
2
+ "version": "0.12.2",
3
3
  "build": 1,
4
4
  "runtimeSdkVersion": "0.10.0",
5
5
  "clients": {
@@ -4,6 +4,7 @@ import type { AssetPackResponse } from './asset-pack.js';
4
4
  import type { AppMetadataAssetSpecSupport, AssetSpecListResponse, AssetSpecResponse, AssetSpecSearchResult, AssetSpecVersionResponse, AssetSpecVersionsListResponse } from './asset-spec.js';
5
5
  import type { AssetCategory, AssetResponse } from './asset.js';
6
6
  import type { ContentLicense, ContentPermissions } from './content-license.js';
7
+ import type { InstrumentEvidenceManifest } from './instrument-evidence.js';
7
8
  import type { AppAuthMode, AppControllerMode, AppHostingMode, AppVersionResponse, AppVersionSummary, AppVersionVisibility, ReviewState } from './version.js';
8
9
  export interface CreateAppRequest {
9
10
  name: string;
@@ -802,6 +803,7 @@ export interface WorkerSubmitAgentTaskReviewRequest {
802
803
  contentType: string;
803
804
  contentBase64: string;
804
805
  }>;
806
+ instrumentEvidence?: InstrumentEvidenceManifest;
805
807
  }
806
808
  export interface WorkerSubmitAgentTaskReviewResponse {
807
809
  task: AgentTaskResponse;
@@ -834,6 +836,13 @@ export interface WorkerSubmitAgentTaskEvalRequest {
834
836
  workerKey?: string;
835
837
  leaseToken?: string;
836
838
  result: GameEvalVerdict;
839
+ instrumentEvidence?: InstrumentEvidenceManifest[];
840
+ evidenceFiles?: Array<{
841
+ groupId: string;
842
+ name: string;
843
+ contentType: string;
844
+ contentBase64: string;
845
+ }>;
837
846
  }
838
847
  export interface WorkerSubmitAgentTaskEvalResponse {
839
848
  task: AgentTaskResponse;
@@ -1674,6 +1683,14 @@ export interface AppAccessTokenResponse {
1674
1683
  appId: number;
1675
1684
  sessionId: string;
1676
1685
  }
1686
+ export interface ReviewerAppAccessTokenResponse extends AppAccessTokenResponse {
1687
+ player: {
1688
+ id: number;
1689
+ username: string;
1690
+ displayName: string;
1691
+ reviewer: true;
1692
+ };
1693
+ }
1677
1694
  export interface DevPlayerSummary {
1678
1695
  userId: number;
1679
1696
  slot: number;