@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.
- package/config/client-meta.json +1 -1
- package/dist/clientInfo.js +0 -11
- package/dist/commands/login.d.ts +13 -1
- package/dist/commands/login.js +72 -5
- package/dist/commands/review.d.ts +0 -20
- package/dist/commands/review.js +1 -112
- package/dist/commands/worker/instrument-evidence.d.ts +17 -0
- package/dist/commands/worker/instrument-evidence.js +239 -0
- package/dist/commands/worker/runtime.d.ts +1 -0
- package/dist/commands/worker/runtime.js +11 -0
- package/dist/commands/worker.d.ts +26 -1
- package/dist/commands/worker.js +263 -10
- package/dist/index.js +21 -17
- package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts +46 -0
- package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts.map +1 -0
- package/node_modules/@playdrop/api-client/dist/domains/game-ideas.js +177 -0
- package/node_modules/@playdrop/api-client/dist/domains/music.d.ts +27 -0
- package/node_modules/@playdrop/api-client/dist/domains/music.d.ts.map +1 -0
- package/node_modules/@playdrop/api-client/dist/domains/music.js +96 -0
- package/node_modules/@playdrop/config/client-meta.json +1 -1
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +17 -0
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
- package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.js +3 -1
- package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts +41 -0
- package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/instrument-evidence.js +215 -0
- 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>;
|
package/dist/commands/worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
},
|
|
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: '
|
|
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
|
|
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.
|
|
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)(
|
|
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.
|
|
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"}
|