@playdrop/playdrop-cli 0.12.1 → 0.12.3

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 (30) hide show
  1. package/config/client-meta.json +1 -1
  2. package/dist/clientInfo.js +0 -11
  3. package/dist/commands/login.d.ts +13 -1
  4. package/dist/commands/login.js +72 -5
  5. package/dist/commands/review.d.ts +0 -20
  6. package/dist/commands/review.js +1 -112
  7. package/dist/commands/worker/instrument-evidence.d.ts +17 -0
  8. package/dist/commands/worker/instrument-evidence.js +239 -0
  9. package/dist/commands/worker/runtime.d.ts +1 -0
  10. package/dist/commands/worker/runtime.js +11 -0
  11. package/dist/commands/worker.d.ts +26 -1
  12. package/dist/commands/worker.js +263 -10
  13. package/dist/index.js +21 -17
  14. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts +46 -0
  15. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts.map +1 -0
  16. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.js +177 -0
  17. package/node_modules/@playdrop/api-client/dist/domains/music.d.ts +27 -0
  18. package/node_modules/@playdrop/api-client/dist/domains/music.d.ts.map +1 -0
  19. package/node_modules/@playdrop/api-client/dist/domains/music.js +96 -0
  20. package/node_modules/@playdrop/config/client-meta.json +1 -1
  21. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  22. package/node_modules/@playdrop/types/dist/api.d.ts +17 -0
  23. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  24. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  25. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  26. package/node_modules/@playdrop/types/dist/index.js +3 -1
  27. package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts +41 -0
  28. package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts.map +1 -0
  29. package/node_modules/@playdrop/types/dist/instrument-evidence.js +215 -0
  30. package/package.json +1 -1
@@ -91,6 +91,16 @@ type TaskFailOptions = {
91
91
  env?: string;
92
92
  message?: string;
93
93
  };
94
+ type TaskEvidenceOptions = {
95
+ evidenceDir?: string;
96
+ group?: string;
97
+ name?: string;
98
+ screenshotId?: string;
99
+ };
100
+ type TaskInstrumentErrorOptions = {
101
+ env?: string;
102
+ reason?: string;
103
+ };
94
104
  type TaskSubmitReviewOptions = {
95
105
  env?: string;
96
106
  state?: string;
@@ -101,6 +111,7 @@ type TaskSubmitReviewOptions = {
101
111
  };
102
112
  type TaskSubmitEvalOptions = {
103
113
  env?: string;
114
+ evidenceDir?: string[];
104
115
  resultFile?: string;
105
116
  };
106
117
  export type WorkerHealthAlertInput = {
@@ -251,17 +262,31 @@ export declare function assertLaunchAgentPlistCompatible(input: {
251
262
  }): void;
252
263
  export declare function showWorkerStatus(options?: WorkerStatusOptions): Promise<WorkerStatusPayload>;
253
264
  export declare function setupWorker(options?: WorkerSetupOptions): Promise<void>;
265
+ export declare function instrumentTaskChromeTitleMatchers(assignment: WorkerAgentTaskAssignmentResponse): string[];
266
+ type InstrumentBrowserHygieneRunner = (command: string, args: string[]) => Promise<{
267
+ stdout: string;
268
+ stderr: string;
269
+ }>;
270
+ export declare function enforceInstrumentBrowserHygiene(assignment: WorkerAgentTaskAssignmentResponse, options?: {
271
+ platform?: NodeJS.Platform;
272
+ runner?: InstrumentBrowserHygieneRunner;
273
+ }): Promise<{
274
+ remainingTaskTabs: number;
275
+ blankWindows: number;
276
+ }>;
254
277
  export declare function startWorker(options?: WorkerStartOptions): Promise<void>;
255
278
  export declare function reportTask(options: TaskReportOptions): Promise<void>;
256
279
  export declare function reportCatalogueTask(options: TaskCatalogueReportOptions): Promise<void>;
257
280
  export declare function uploadTask(options?: TaskUploadOptions): Promise<void>;
258
281
  export declare function claimSlugTask(options: TaskClaimSlugOptions): Promise<void>;
259
282
  export declare function completeTask(options: TaskCompleteOptions): Promise<void>;
260
- export declare function readReviewEvidenceFiles(evidenceDir: string | undefined): Array<{
283
+ export declare function readReviewEvidenceFiles(evidenceDir: string | undefined, workspaceDir?: string): Array<{
261
284
  name: string;
262
285
  contentType: string;
263
286
  contentBase64: string;
264
287
  }>;
265
288
  export declare function submitReviewTask(options: TaskSubmitReviewOptions): Promise<void>;
266
289
  export declare function submitEvalTask(options: TaskSubmitEvalOptions): Promise<void>;
290
+ export declare function saveTaskEvidence(options: TaskEvidenceOptions): Promise<void>;
291
+ export declare function instrumentErrorTask(options: TaskInstrumentErrorOptions): Promise<void>;
267
292
  export declare function failTask(options: TaskFailOptions): Promise<void>;
@@ -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
@@ -120,6 +120,7 @@ function registerLoginCommand(parent) {
120
120
  .option('--password <password>', 'Password for direct login')
121
121
  .option('--key <apiKey>', 'API key for direct login')
122
122
  .option('--handoff-token <token>', 'Native app handoff token for direct login')
123
+ .option('--handoff-token-stdin', 'Read a native app handoff token from stdin')
123
124
  .option('--json', 'Output JSON')
124
125
  .action(async (opts) => {
125
126
  await (0, login_1.login)(opts.env, {
@@ -127,6 +128,7 @@ function registerLoginCommand(parent) {
127
128
  password: opts.password,
128
129
  key: opts.key,
129
130
  handoffToken: opts.handoffToken,
131
+ handoffTokenStdin: opts.handoffTokenStdin,
130
132
  json: opts.json,
131
133
  });
132
134
  });
@@ -276,10 +278,29 @@ task
276
278
  .command('submit-eval')
277
279
  .description('Submit the active game eval task result')
278
280
  .requiredOption('--result-file <path>', 'Strict GAME_EVAL JSON result file')
281
+ .requiredOption('--evidence-dir <paths...>', 'One instrument evidence directory per eval target')
279
282
  .option('--env <env>', 'Environment override')
280
283
  .action(async (opts) => {
281
284
  await (0, worker_1.submitEvalTask)(opts);
282
285
  });
286
+ task
287
+ .command('evidence')
288
+ .description('Persist a Claude Chrome screenshot as task evidence')
289
+ .requiredOption('--name <name>', 'Evidence state: first-frame, core, win, or loss')
290
+ .requiredOption('--screenshot-id <id>', 'Claude Chrome screenshot id')
291
+ .requiredOption('--evidence-dir <path>', 'Evidence output directory')
292
+ .option('--group <id>', 'Evidence group id; required for GAME_EVAL')
293
+ .action(async (opts) => {
294
+ await (0, worker_1.saveTaskEvidence)(opts);
295
+ });
296
+ task
297
+ .command('instrument-error')
298
+ .description('Terminate a review or eval task with a structured instrument error')
299
+ .requiredOption('--reason <reason>', 'auth_state, browser_control, renderer, or screenshot')
300
+ .option('--env <env>', 'Environment override')
301
+ .action(async (opts) => {
302
+ await (0, worker_1.instrumentErrorTask)(opts);
303
+ });
283
304
  task
284
305
  .command('fail')
285
306
  .description('Fail the active agent task')
@@ -289,23 +310,6 @@ task
289
310
  await (0, worker_1.failTask)(opts);
290
311
  });
291
312
  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
313
  review
310
314
  .command('validate-result')
311
315
  .description('Validate a canonical game review result')
@@ -0,0 +1,46 @@
1
+ import type { CreateGameIdeaRequest, CreateGameIdeaResponse, GameIdeaDetailResponse, GameIdeaStatusResponse, GameIdeasListRequest, GameIdeasListResponse, RemixCandidatesResponse, ShipGameIdeaRequest, ShipGameIdeaResponse, StartGameIdeaRequest, StartGameIdeaResponse, UpdateGameIdeaRequest, UpdateGameIdeaResponse } from '@playdrop/types';
2
+ type ApiResponseLike<T> = {
3
+ status: number;
4
+ body: T;
5
+ headers: Headers;
6
+ };
7
+ type RequestExecutor = <T>(input: {
8
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
+ path: string;
10
+ body?: unknown;
11
+ headers?: Record<string, string>;
12
+ signal?: AbortSignal;
13
+ }) => Promise<ApiResponseLike<T>>;
14
+ type ErrorHandler = (response: ApiResponseLike<unknown>, defaultMessage: string) => never;
15
+ type ParseResponseBody = <T>(response: Response) => Promise<T>;
16
+ export type CreateGameIdeaOptions = {
17
+ payload: CreateGameIdeaRequest;
18
+ image?: File | Blob;
19
+ filename?: string;
20
+ };
21
+ export declare function buildGameIdeasApiClientMethods(input: {
22
+ request: RequestExecutor;
23
+ handleApiError: ErrorHandler;
24
+ fetchImpl: typeof fetch;
25
+ includeBrowserCredentials: boolean;
26
+ resolveBaseUrl: () => string;
27
+ resolveUrl: (base: string, path: string) => string;
28
+ parseResponseBody: ParseResponseBody;
29
+ resolveToken: () => Promise<string | null>;
30
+ getClientHeaders: () => Record<string, string>;
31
+ }): {
32
+ createGameIdea(options: CreateGameIdeaOptions): Promise<CreateGameIdeaResponse>;
33
+ listGameIdeas(options?: GameIdeasListRequest): Promise<GameIdeasListResponse>;
34
+ getGameIdea(idOrSlug: string): Promise<GameIdeaDetailResponse>;
35
+ getGameIdeaStatus(idOrSlug: string): Promise<GameIdeaStatusResponse>;
36
+ updateGameIdea(idOrSlug: string, body: UpdateGameIdeaRequest): Promise<UpdateGameIdeaResponse>;
37
+ startGameIdea(idOrSlug: string, body?: StartGameIdeaRequest): Promise<StartGameIdeaResponse>;
38
+ shipGameIdea(idOrSlug: string, body: ShipGameIdeaRequest): Promise<ShipGameIdeaResponse>;
39
+ fetchRemixCandidates(options?: {
40
+ q?: string;
41
+ limit?: number;
42
+ offset?: number;
43
+ }): Promise<RemixCandidatesResponse>;
44
+ };
45
+ export {};
46
+ //# sourceMappingURL=game-ideas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-ideas.d.ts","sourceRoot":"","sources":["../../src/domains/game-ideas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,iBAAiB,CAAC;AAGzB,KAAK,eAAe,CAAC,CAAC,IAAI;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,CAAC,CAAC;IACR,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,KAAK,eAAe,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE;IAChC,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;AAElC,KAAK,YAAY,GAAG,CAAC,QAAQ,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,KAAK,KAAK,CAAC;AAE1F,KAAK,iBAAiB,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAE/D,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,KAAK,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAqCF,wBAAgB,8BAA8B,CAAC,KAAK,EAAE;IACpD,OAAO,EAAE,eAAe,CAAC;IACzB,cAAc,EAAE,YAAY,CAAC;IAC7B,SAAS,EAAE,OAAO,KAAK,CAAC;IACxB,yBAAyB,EAAE,OAAO,CAAC;IACnC,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACnD,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3C,gBAAgB,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChD;4BAciC,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC;4BAyBxD,oBAAoB,GAAQ,OAAO,CAAC,qBAAqB,CAAC;0BAiB3D,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;gCAYlC,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;6BAY3C,MAAM,QAAQ,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC;4BAatE,MAAM,SAAQ,oBAAoB,GAAQ,OAAO,CAAC,qBAAqB,CAAC;2BAczE,MAAM,QAAQ,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;mCAiB1D;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAQ,OAAO,CAAC,uBAAuB,CAAC;EAiB9H"}