@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.
- package/config/client-meta.json +1 -1
- 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 +19 -17
- package/node_modules/@playdrop/config/client-meta.json +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
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
|
@@ -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')
|
|
@@ -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;
|