@playdrop/playdrop-cli 0.12.37 → 0.12.39

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 (34) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/build.d.ts +3 -1
  3. package/dist/apps/build.js +4 -0
  4. package/dist/apps/loadCheck.d.ts +9 -2
  5. package/dist/apps/loadCheck.js +167 -32
  6. package/dist/apps/surfaceProfiles.d.ts +10 -0
  7. package/dist/apps/surfaceProfiles.js +64 -0
  8. package/dist/apps/upload.js +2 -0
  9. package/dist/catalogue.d.ts +7 -1
  10. package/dist/catalogue.js +27 -0
  11. package/dist/commands/check.d.ts +1 -0
  12. package/dist/commands/check.js +68 -2
  13. package/dist/commands/upload.d.ts +1 -0
  14. package/dist/commands/upload.js +117 -22
  15. package/dist/commands/worker/runtime.d.ts +6 -1
  16. package/dist/commands/worker/runtime.js +20 -3
  17. package/dist/commands/worker.d.ts +29 -3
  18. package/dist/commands/worker.js +114 -15
  19. package/dist/index.js +2 -0
  20. package/node_modules/@playdrop/config/client-meta.json +2 -2
  21. package/node_modules/@playdrop/types/dist/api.d.ts +23 -1
  22. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  23. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts +49 -0
  24. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts.map +1 -0
  25. package/node_modules/@playdrop/types/dist/app-playtest-tape.js +242 -0
  26. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  27. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  28. package/node_modules/@playdrop/types/dist/index.js +2 -0
  29. package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts +3 -0
  30. package/node_modules/@playdrop/types/dist/instrument-evidence.d.ts.map +1 -1
  31. package/node_modules/@playdrop/types/dist/instrument-evidence.js +56 -5
  32. package/node_modules/@playdrop/types/dist/version.d.ts +5 -0
  33. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  34. package/package.json +1 -1
@@ -4,6 +4,7 @@ exports.resolveUploadOwner = resolveUploadOwner;
4
4
  exports.resolveDefaultUploadCreator = resolveDefaultUploadCreator;
5
5
  exports.upload = upload;
6
6
  exports.resolveWorkerPlaydropAssetRequirement = resolveWorkerPlaydropAssetRequirement;
7
+ exports.assertAgentNewGamePlaytestContract = assertAgentNewGamePlaytestContract;
7
8
  exports.assertTaskPlaytestEvidenceManifest = assertTaskPlaytestEvidenceManifest;
8
9
  exports.assertWorkerAppLocalUploadPreflight = assertWorkerAppLocalUploadPreflight;
9
10
  exports.publishWorkerAppProject = publishWorkerAppProject;
@@ -1222,6 +1223,22 @@ function resolveWorkerPlaydropAssetRequirement(value) {
1222
1223
  }
1223
1224
  return null;
1224
1225
  }
1226
+ function assertAgentNewGamePlaytestContract(task) {
1227
+ if (!task.primarySurface) {
1228
+ throw new Error('agent_task_primary_surface_required: NEW_GAME catalogue.json must declare primarySurface.');
1229
+ }
1230
+ if (!task.surfaceTargets.includes(task.primarySurface)) {
1231
+ throw new Error(`agent_task_primary_surface_not_supported:${task.primarySurface}`);
1232
+ }
1233
+ if (!task.playtestTapes) {
1234
+ throw new Error('agent_task_playtest_tapes_required: NEW_GAME catalogue.json must declare one playtest tape per supported surface.');
1235
+ }
1236
+ for (const surface of task.surfaceTargets) {
1237
+ if (!task.playtestTapes[surface]) {
1238
+ throw new Error(`agent_task_playtest_tape_missing:${surface}`);
1239
+ }
1240
+ }
1241
+ }
1225
1242
  const WORKER_SUPERVISOR_SOURCE_EXCLUDES = [
1226
1243
  'PLAYDROP_TASK.md',
1227
1244
  'AGENTS.md',
@@ -1670,6 +1687,70 @@ function readPlaytestEvidenceManifest(task) {
1670
1687
  throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must be valid JSON. ${reason}`));
1671
1688
  }
1672
1689
  }
1690
+ function assertGreyboxReport(task) {
1691
+ const reportPath = (0, node_path_1.join)(task.projectDir, 'greybox-report.json');
1692
+ if (!(0, node_fs_1.existsSync)(reportPath)) {
1693
+ throw new Error('agent_task_greybox_report_missing: greybox-report.json must prove the prototype and final start, agency, and restart checks.');
1694
+ }
1695
+ let report;
1696
+ try {
1697
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(reportPath, 'utf8'));
1698
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1699
+ throw new Error('report must be a JSON object');
1700
+ }
1701
+ report = parsed;
1702
+ }
1703
+ catch (error) {
1704
+ throw new Error(`agent_task_greybox_report_invalid: ${error instanceof Error ? error.message : String(error)}`);
1705
+ }
1706
+ if (report.schemaVersion !== 1 || Object.keys(report).some((key) => !['schemaVersion', 'prototype', 'final'].includes(key))) {
1707
+ throw new Error('agent_task_greybox_report_invalid: expected schemaVersion 1 with only prototype and final sections.');
1708
+ }
1709
+ for (const sectionName of ['prototype', 'final']) {
1710
+ const rawSection = report[sectionName];
1711
+ if (!rawSection || typeof rawSection !== 'object' || Array.isArray(rawSection)) {
1712
+ throw new Error(`agent_task_greybox_report_invalid: ${sectionName} must contain start, agency, and restart.`);
1713
+ }
1714
+ const section = rawSection;
1715
+ if (Object.keys(section).length !== 3
1716
+ || Object.keys(section).some((key) => !['start', 'agency', 'restart'].includes(key))) {
1717
+ throw new Error(`agent_task_greybox_report_invalid: ${sectionName} must contain exactly start, agency, and restart.`);
1718
+ }
1719
+ for (const checkName of ['start', 'agency', 'restart']) {
1720
+ const rawCheck = section[checkName];
1721
+ if (!rawCheck || typeof rawCheck !== 'object' || Array.isArray(rawCheck)) {
1722
+ throw new Error(`agent_task_greybox_report_invalid: ${sectionName}.${checkName} must be an object.`);
1723
+ }
1724
+ const check = rawCheck;
1725
+ const expectedKeys = checkName === 'agency'
1726
+ ? ['passed', 'normalInput', 'controlCondition', 'observation']
1727
+ : ['passed', 'observation'];
1728
+ if (Object.keys(check).length !== expectedKeys.length
1729
+ || Object.keys(check).some((key) => !expectedKeys.includes(key))
1730
+ || check.passed !== true
1731
+ || typeof check.observation !== 'string'
1732
+ || !check.observation.trim()
1733
+ || (checkName === 'agency' && (typeof check.normalInput !== 'string'
1734
+ || !check.normalInput.trim()
1735
+ || typeof check.controlCondition !== 'string'
1736
+ || !check.controlCondition.trim()))) {
1737
+ throw new Error(`agent_task_greybox_report_invalid: ${sectionName}.${checkName} must be complete and passing.`);
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ async function collectUploadPreflightErrors(checks) {
1743
+ const errors = [];
1744
+ for (const check of checks) {
1745
+ try {
1746
+ await check();
1747
+ }
1748
+ catch (error) {
1749
+ errors.push(error instanceof Error ? error.message : String(error));
1750
+ }
1751
+ }
1752
+ return errors;
1753
+ }
1673
1754
  const PLAYTEST_EVIDENCE_MANIFEST_HELP = 'Expected shape: {"version":1,"entries":[{"environment":"local","url":"http://localhost:8080/...","surface":"desktop","checkedAt":"2026-07-05T00:00:00.000Z","captures":["evidence/input.png","evidence/win.png","evidence/loss.png"],"actions":["press ArrowRight","complete the final round","allow health to reach zero"],"statesObserved":["player moved right","victory overlay","defeat overlay"],"consoleErrors":[]}],"proof":{"primaryInput":{"action":"press ArrowRight","observed":"player moved right","capture":"evidence/input.png"},"win":{"action":"complete the final round","observed":"victory overlay appeared","capture":"evidence/win.png"},"loss":{"action":"allow health to reach zero","observed":"defeat overlay appeared","capture":"evidence/loss.png"}}}. NEW_GAME proof requires three distinct captures from a current non-final entry. The uploader appends hashes and the final published-route entry automatically.';
1674
1755
  function formatMissingPlaytestEvidenceManifestError(message) {
1675
1756
  return `agent_task_playtest_manifest_missing: ${message} ${PLAYTEST_EVIDENCE_MANIFEST_HELP}`;
@@ -1816,30 +1897,44 @@ async function assertWorkerAppLocalUploadPreflight(input) {
1816
1897
  if (input.kind === 'GAME_UPDATE') {
1817
1898
  return assertTaskPlaytestEvidenceManifest(input.task);
1818
1899
  }
1819
- (0, listingPreflight_1.assertAgentGameHeroListingPreflight)(input.task);
1820
- if (input.kind === 'NEW_GAME') {
1821
- (0, listingPreflight_1.assertAgentNewGameArtDirectionPreflight)(input.task);
1900
+ let playtestProof = null;
1901
+ const errors = await collectUploadPreflightErrors([
1902
+ () => assertGreyboxReport(input.task),
1903
+ () => (0, listingPreflight_1.assertAgentGameHeroListingPreflight)(input.task),
1904
+ ...(input.kind === 'NEW_GAME' ? [() => (0, listingPreflight_1.assertAgentNewGameArtDirectionPreflight)(input.task)] : []),
1905
+ ...(input.kind === 'NEW_GAME' ? [() => assertAgentNewGamePlaytestContract(input.task)] : []),
1906
+ () => {
1907
+ if (!appTaskSatisfiesPlaydropAssetRequirement(input.task, input.playdropAssetRequirement)) {
1908
+ throw new Error('agent_task_playdrop_asset_dependency_missing: the creator asked for PlayDrop assets, but catalogue.json does not declare a matching uses.assets, uses.packs, or ownedAssets dependency.');
1909
+ }
1910
+ },
1911
+ () => (0, ownedRuntimeImageValidation_1.assertOwnedRuntimeImagesAreValid)(input.task),
1912
+ () => assertRequestedPlaydropAssetsAreUsedAtRuntime({
1913
+ task: input.task,
1914
+ requirement: input.playdropAssetRequirement,
1915
+ creatorRequest: input.creatorRequest,
1916
+ }),
1917
+ () => assertNewGameDeclaredAssetsAreUsedAtRuntime({
1918
+ creatorUsername: input.creatorUsername?.trim() ?? '',
1919
+ task: input.task,
1920
+ }),
1921
+ () => (0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(input.task),
1922
+ () => (0, listingPreflight_1.assertAgentGameCaptureReportPreflight)(input.task, {
1923
+ requireCaptureReport: input.executionTarget === 'FIRST_PARTY',
1924
+ }),
1925
+ () => {
1926
+ playtestProof = assertTaskPlaytestEvidenceManifest(input.task, {
1927
+ requireOutcomeProof: input.kind === 'NEW_GAME',
1928
+ });
1929
+ },
1930
+ ]);
1931
+ if (errors.length === 1) {
1932
+ throw new Error(errors[0]);
1822
1933
  }
1823
- if (!appTaskSatisfiesPlaydropAssetRequirement(input.task, input.playdropAssetRequirement)) {
1824
- throw new Error('agent_task_playdrop_asset_dependency_missing: the creator asked for PlayDrop assets, but catalogue.json does not declare a matching uses.assets, uses.packs, or ownedAssets dependency.');
1934
+ if (errors.length > 1) {
1935
+ throw new Error(`agent_task_upload_preflight_failed:\n${errors.map((error, index) => `${index + 1}. ${error}`).join('\n')}`);
1825
1936
  }
1826
- await (0, ownedRuntimeImageValidation_1.assertOwnedRuntimeImagesAreValid)(input.task);
1827
- assertRequestedPlaydropAssetsAreUsedAtRuntime({
1828
- task: input.task,
1829
- requirement: input.playdropAssetRequirement,
1830
- creatorRequest: input.creatorRequest,
1831
- });
1832
- await assertNewGameDeclaredAssetsAreUsedAtRuntime({
1833
- creatorUsername: input.creatorUsername?.trim() ?? '',
1834
- task: input.task,
1835
- });
1836
- (0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(input.task);
1837
- (0, listingPreflight_1.assertAgentGameCaptureReportPreflight)(input.task, {
1838
- requireCaptureReport: input.executionTarget === 'FIRST_PARTY',
1839
- });
1840
- return assertTaskPlaytestEvidenceManifest(input.task, {
1841
- requireOutcomeProof: input.kind === 'NEW_GAME',
1842
- });
1937
+ return playtestProof;
1843
1938
  }
1844
1939
  // Task upload path used by "playdrop task upload": same internal pipeline as
1845
1940
  // "playdrop project publish", but always as one PRIVATE draft version for the
@@ -77,7 +77,12 @@ export declare function buildClaudeExecArgs(input: {
77
77
  pluginDir?: string | null;
78
78
  }): string[];
79
79
  export declare const PLAYWRIGHT_MCP_VERSION = "0.0.78";
80
- export declare function buildPlaywrightMcpServerConfig(): Record<string, unknown>;
80
+ export type WorkerInstrumentSurface = 'DESKTOP' | 'MOBILE_LANDSCAPE' | 'MOBILE_PORTRAIT';
81
+ export declare function buildPlaywrightMcpCommand(surface?: WorkerInstrumentSurface | null): {
82
+ command: string;
83
+ args: string[];
84
+ };
85
+ export declare function buildPlaywrightMcpServerConfig(surface?: WorkerInstrumentSurface | null): Record<string, unknown>;
81
86
  export declare function buildClaudePermissionSettings(input?: {
82
87
  workspaceDir?: string | null;
83
88
  denyReadRoots?: string[];
@@ -17,6 +17,7 @@ exports.runLoggedProcess = runLoggedProcess;
17
17
  exports.buildCodexExecArgs = buildCodexExecArgs;
18
18
  exports.readCodexSandboxMode = readCodexSandboxMode;
19
19
  exports.buildClaudeExecArgs = buildClaudeExecArgs;
20
+ exports.buildPlaywrightMcpCommand = buildPlaywrightMcpCommand;
20
21
  exports.buildPlaywrightMcpServerConfig = buildPlaywrightMcpServerConfig;
21
22
  exports.buildClaudePermissionSettings = buildClaudePermissionSettings;
22
23
  exports.buildClaudeDeniedPermissionRules = buildClaudeDeniedPermissionRules;
@@ -25,6 +26,7 @@ const node_child_process_1 = require("node:child_process");
25
26
  const promises_1 = require("node:fs/promises");
26
27
  const node_os_1 = __importDefault(require("node:os"));
27
28
  const node_path_1 = __importDefault(require("node:path"));
29
+ const surfaceProfiles_1 = require("../../apps/surfaceProfiles");
28
30
  const CHILD_KILL_GRACE_MS = 10000;
29
31
  exports.DEFAULT_CODEX_TIMEOUT_MS = 2 * 60 * 60 * 1000;
30
32
  exports.DEFAULT_WORKER_TOKEN_CAP = 20000000;
@@ -647,12 +649,27 @@ const CLAUDE_BASE_TOOL_NAMES = [
647
649
  // never through @latest drift (see the 2026-07-13 auto-update incident).
648
650
  exports.PLAYWRIGHT_MCP_VERSION = '0.0.78';
649
651
  const PLAYWRIGHT_MCP_ALLOWED_TOOL_RULE = 'mcp__playwright';
650
- function buildPlaywrightMcpServerConfig() {
652
+ function buildPlaywrightMcpCommand(surface) {
653
+ const surfaceArgs = surface ? (0, surfaceProfiles_1.buildPlaywrightMcpSurfaceArgs)(surface) : [];
654
+ return {
655
+ command: 'npx',
656
+ args: [
657
+ '-y',
658
+ `@playwright/mcp@${exports.PLAYWRIGHT_MCP_VERSION}`,
659
+ '--browser',
660
+ 'chrome',
661
+ '--isolated',
662
+ ...surfaceArgs,
663
+ ],
664
+ };
665
+ }
666
+ function buildPlaywrightMcpServerConfig(surface) {
667
+ const server = buildPlaywrightMcpCommand(surface);
651
668
  return {
652
669
  mcpServers: {
653
670
  playwright: {
654
- command: 'npx',
655
- args: ['-y', `@playwright/mcp@${exports.PLAYWRIGHT_MCP_VERSION}`, '--browser', 'chrome', '--isolated'],
671
+ command: server.command,
672
+ args: server.args,
656
673
  },
657
674
  },
658
675
  };
@@ -1,9 +1,10 @@
1
1
  import type { ApiClient } from '@playdrop/api-client';
2
- import { type AgentExecutionTarget, type AgentRuntime, type AgentTaskNextStepSuggestion, type AgentTaskResponse, type AgentWorkerCapabilities, type AgentWorkerResponse, type AgentWorkerUpdatePolicyResponse, type WorkerTaskAssignmentV2, type WorkerTaskPluginV2, type WorkerClaimAgentTaskResponse } from '@playdrop/types';
2
+ import { type AgentExecutionTarget, type AgentRuntime, type AgentTaskKind, type AgentTaskNextStepSuggestion, type AgentTaskResponse, type AgentWorkerCapabilities, type AgentWorkerResponse, type AgentWorkerUpdatePolicyResponse, type JsonValue, type WorkerTaskAssignmentV2, type WorkerTaskPluginV2, type WorkerClaimAgentTaskResponse } from '@playdrop/types';
3
3
  import { loadConfig } from '../config';
4
+ import { runShell } from '../shellProbe';
4
5
  import { type WorkerPlaydropAssetRequirement } from './upload';
5
- import { type LoggedProcessResult, type LoggedProcessTranscriptChunk } from './worker/runtime';
6
- export { assertWorkerTokenUsageWithinCap, buildClaudeExecArgs, buildClaudePermissionSettings, buildCodexExecArgs, buildWorkerChildEnv, DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, extractAgentTokenUsage, extractCodexRolloutTokenUsage, extractCodexThreadId, extractCodexTokensUsed, readCodexRolloutTokenUsageForThread, readCodexSandboxMode, readEnvBoolean, runLoggedProcess } from './worker/runtime';
6
+ import { type WorkerInstrumentSurface, type LoggedProcessResult, type LoggedProcessTranscriptChunk } from './worker/runtime';
7
+ export { assertWorkerTokenUsageWithinCap, buildClaudeExecArgs, buildClaudePermissionSettings, buildCodexExecArgs, buildPlaywrightMcpCommand, buildWorkerChildEnv, DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, extractAgentTokenUsage, extractCodexRolloutTokenUsage, extractCodexThreadId, extractCodexTokensUsed, readCodexRolloutTokenUsageForThread, readCodexSandboxMode, readEnvBoolean, runLoggedProcess } from './worker/runtime';
7
8
  export type { AgentTokenUsage, LoggedProcessResult, LoggedProcessTranscriptChunk } from './worker/runtime';
8
9
  export declare const WORKER_SESSION_EXPIRED_MESSAGE = "worker session expired: run \"playdrop auth login\" and start the worker again";
9
10
  export declare const WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = "worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task claim-slug, task upload/done/fail, assigned app scaffolding, project validation/build/dev/check/capture, read-only help/catalogue/documentation lookup, and PlayDrop AI generation are permitted.";
@@ -126,6 +127,24 @@ type WorkerTaskState = {
126
127
  leaseExpiresAt: string | null;
127
128
  claimedAt: string;
128
129
  };
130
+ type WorkerTaskContextFile = {
131
+ taskId: number;
132
+ attempt: number;
133
+ taskToken: string;
134
+ taskType: string;
135
+ metadata: Record<string, JsonValue>;
136
+ kind: AgentTaskKind;
137
+ creatorUsername: string;
138
+ creatorRequest: string;
139
+ target: AgentExecutionTarget;
140
+ outputAppName: string | null;
141
+ outputVersion: string;
142
+ remixSourceRef?: string | null;
143
+ allowedTemplateKeys?: string[];
144
+ reviewAppVersionId?: number | null;
145
+ devPort: number;
146
+ env: string;
147
+ };
129
148
  type WorkerCodexModel = {
130
149
  model: string;
131
150
  reasoningEffort: string;
@@ -147,6 +166,7 @@ export declare function claimBackoffDelayMs(currentMs: number, random?: () => nu
147
166
  export declare function resolveWorkerHomeDir(): string;
148
167
  export declare const WORKER_TASK_WORKSPACE_RETENTION = 20;
149
168
  export declare function pruneWorkerTaskWorkspaces(incomingWorkspaceDir: string, retention?: number): Promise<string[]>;
169
+ export declare function resolveWorkerInstrumentSurface(taskContext: Pick<WorkerTaskContextFile, 'kind' | 'metadata'>): WorkerInstrumentSurface | null;
150
170
  export declare function hasAgentTaskUploadedArtifact(task: Pick<AgentTaskResponse, 'appId' | 'appVersionId'>): boolean;
151
171
  export declare function appendWorkerTranscriptChunks(input: {
152
172
  client: Pick<ApiClient, 'workerAppendAgentTaskTranscriptChunks'>;
@@ -176,6 +196,7 @@ export declare function assertAssignmentPluginBundleExtractionBounds(buffer: Buf
176
196
  export declare function stageAssignmentPluginBundle(input: {
177
197
  workspaceDir: string;
178
198
  pluginBundle: WorkerTaskPluginV2;
199
+ taskKind?: AgentTaskKind;
179
200
  }): Promise<string>;
180
201
  export declare function resolveDevPluginWorkingTree(input: {
181
202
  workingTree?: string;
@@ -328,6 +349,11 @@ export declare function assertLaunchAgentPlistCompatible(input: {
328
349
  wrapperPath: string;
329
350
  canonicalPlistPath: string;
330
351
  }): void;
352
+ export declare function launchManagedWorker(label: string, plistPath: string, options: {
353
+ registrationChanged: boolean;
354
+ runCommand?: typeof runShell;
355
+ uid?: number;
356
+ }): void;
331
357
  export declare function showWorkerStatus(options?: WorkerStatusOptions): Promise<WorkerStatusPayload>;
332
358
  export declare function setupWorker(options?: WorkerSetupOptions): Promise<void>;
333
359
  export declare function instrumentTaskChromeTitleMatchers(assignment: WorkerTaskAssignmentV2): string[];
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readEnvBoolean = exports.readCodexSandboxMode = exports.readCodexRolloutTokenUsageForThread = exports.extractCodexTokensUsed = exports.extractCodexThreadId = exports.extractCodexRolloutTokenUsage = exports.extractAgentTokenUsage = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = exports.buildWorkerChildEnv = exports.buildCodexExecArgs = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.assertWorkerTokenUsageWithinCap = void 0;
6
+ exports.WORKER_TASK_WORKSPACE_RETENTION = exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readEnvBoolean = exports.readCodexSandboxMode = exports.readCodexRolloutTokenUsageForThread = exports.extractCodexTokensUsed = exports.extractCodexThreadId = exports.extractCodexRolloutTokenUsage = exports.extractAgentTokenUsage = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = exports.buildWorkerChildEnv = exports.buildPlaywrightMcpCommand = exports.buildCodexExecArgs = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.assertWorkerTokenUsageWithinCap = void 0;
7
7
  exports.allocateWorkerDevPort = allocateWorkerDevPort;
8
8
  exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
9
9
  exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
@@ -12,6 +12,7 @@ exports.nextClaimBackoffMs = nextClaimBackoffMs;
12
12
  exports.claimBackoffDelayMs = claimBackoffDelayMs;
13
13
  exports.resolveWorkerHomeDir = resolveWorkerHomeDir;
14
14
  exports.pruneWorkerTaskWorkspaces = pruneWorkerTaskWorkspaces;
15
+ exports.resolveWorkerInstrumentSurface = resolveWorkerInstrumentSurface;
15
16
  exports.hasAgentTaskUploadedArtifact = hasAgentTaskUploadedArtifact;
16
17
  exports.appendWorkerTranscriptChunks = appendWorkerTranscriptChunks;
17
18
  exports.readTaskNextStepsFile = readTaskNextStepsFile;
@@ -65,6 +66,7 @@ exports.withTaskUploadLock = withTaskUploadLock;
65
66
  exports.readActivePersonalWorkerRuntimeState = readActivePersonalWorkerRuntimeState;
66
67
  exports.parseLaunchAgentProgramArguments = parseLaunchAgentProgramArguments;
67
68
  exports.assertLaunchAgentPlistCompatible = assertLaunchAgentPlistCompatible;
69
+ exports.launchManagedWorker = launchManagedWorker;
68
70
  exports.showWorkerStatus = showWorkerStatus;
69
71
  exports.setupWorker = setupWorker;
70
72
  exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
@@ -109,6 +111,7 @@ Object.defineProperty(exports, "assertWorkerTokenUsageWithinCap", { enumerable:
109
111
  Object.defineProperty(exports, "buildClaudeExecArgs", { enumerable: true, get: function () { return runtime_2.buildClaudeExecArgs; } });
110
112
  Object.defineProperty(exports, "buildClaudePermissionSettings", { enumerable: true, get: function () { return runtime_2.buildClaudePermissionSettings; } });
111
113
  Object.defineProperty(exports, "buildCodexExecArgs", { enumerable: true, get: function () { return runtime_2.buildCodexExecArgs; } });
114
+ Object.defineProperty(exports, "buildPlaywrightMcpCommand", { enumerable: true, get: function () { return runtime_2.buildPlaywrightMcpCommand; } });
112
115
  Object.defineProperty(exports, "buildWorkerChildEnv", { enumerable: true, get: function () { return runtime_2.buildWorkerChildEnv; } });
113
116
  Object.defineProperty(exports, "DEFAULT_CODEX_TIMEOUT_MS", { enumerable: true, get: function () { return runtime_2.DEFAULT_CODEX_TIMEOUT_MS; } });
114
117
  Object.defineProperty(exports, "DEFAULT_WORKER_TOKEN_CAP", { enumerable: true, get: function () { return runtime_2.DEFAULT_WORKER_TOKEN_CAP; } });
@@ -148,6 +151,7 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
148
151
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
149
152
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
150
153
  ['task', 'context'],
154
+ ['task', 'help'],
151
155
  ['task', 'report'],
152
156
  ['task', 'report-catalogue'],
153
157
  ['task', 'claim-slug'],
@@ -170,6 +174,7 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
170
174
  ['search'],
171
175
  ['detail'],
172
176
  ['versions', 'browse'],
177
+ ['tags', 'browse'],
173
178
  ['documentation', 'browse'],
174
179
  ['documentation', 'read'],
175
180
  ['help'],
@@ -567,6 +572,48 @@ function buildWorkerTaskContextV2(assignment) {
567
572
  target: assignment.task.executionTarget,
568
573
  });
569
574
  }
575
+ function normalizeWorkerInstrumentSurface(value) {
576
+ return value === 'DESKTOP' || value === 'MOBILE_LANDSCAPE' || value === 'MOBILE_PORTRAIT'
577
+ ? value
578
+ : null;
579
+ }
580
+ function resolveWorkerInstrumentSurface(taskContext) {
581
+ const playdrop = taskContext.metadata.playdrop;
582
+ if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
583
+ return null;
584
+ }
585
+ const metadata = playdrop;
586
+ if (taskContext.kind === 'GAME_REVIEW') {
587
+ const reviewTarget = metadata.reviewTarget;
588
+ if (!reviewTarget || typeof reviewTarget !== 'object' || Array.isArray(reviewTarget)) {
589
+ return null;
590
+ }
591
+ return normalizeWorkerInstrumentSurface(reviewTarget.primarySurface);
592
+ }
593
+ if (taskContext.kind === 'GAME_EVAL') {
594
+ const evalTarget = metadata.evalTarget;
595
+ if (!evalTarget || typeof evalTarget !== 'object' || Array.isArray(evalTarget)) {
596
+ return null;
597
+ }
598
+ const targets = evalTarget.targets;
599
+ if (!Array.isArray(targets) || targets.length === 0) {
600
+ return null;
601
+ }
602
+ const surfaces = new Set();
603
+ for (const target of targets) {
604
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
605
+ return null;
606
+ }
607
+ const surface = normalizeWorkerInstrumentSurface(target.primarySurface);
608
+ if (!surface) {
609
+ return null;
610
+ }
611
+ surfaces.add(surface);
612
+ }
613
+ return surfaces.size === 1 ? Array.from(surfaces)[0] ?? null : null;
614
+ }
615
+ return null;
616
+ }
570
617
  function workerTaskContextPath(workspaceDir) {
571
618
  return node_path_1.default.join(workspaceDir, '.playdrop', 'task.json');
572
619
  }
@@ -1062,9 +1109,24 @@ function readAgentBundleManifest(extractedRoot) {
1062
1109
  if (!destinations.has('CLAUDE.md')) {
1063
1110
  throw new Error('agent_bundle_claude_file_missing');
1064
1111
  }
1112
+ const taskAgentOverlays = manifest.workspace.taskAgentOverlays;
1113
+ if (taskAgentOverlays !== undefined) {
1114
+ if (!taskAgentOverlays || typeof taskAgentOverlays !== 'object' || Array.isArray(taskAgentOverlays)) {
1115
+ throw new Error('agent_bundle_task_agent_overlays_invalid');
1116
+ }
1117
+ for (const [rawKind, rawSource] of Object.entries(taskAgentOverlays)) {
1118
+ if (!WORKER_SUPPORTED_KINDS.includes(rawKind)) {
1119
+ throw new Error(`agent_bundle_task_agent_overlay_kind_invalid:${rawKind}`);
1120
+ }
1121
+ const source = normalizeAgentBundlePath(extractedRoot, rawSource, 'agent_bundle_task_agent_overlay_source_invalid');
1122
+ if (!isFile(source)) {
1123
+ throw new Error(`agent_bundle_task_agent_overlay_source_missing:${rawSource ?? ''}`);
1124
+ }
1125
+ }
1126
+ }
1065
1127
  return { manifest, pluginRoot };
1066
1128
  }
1067
- async function stageAgentBundleWorkspaceFiles(extractedRoot, workspaceDir) {
1129
+ async function stageAgentBundleWorkspaceFiles(extractedRoot, workspaceDir, taskKind) {
1068
1130
  const resolved = readAgentBundleManifest(extractedRoot);
1069
1131
  for (const entry of resolved.manifest.workspace.files) {
1070
1132
  const source = normalizeAgentBundlePath(extractedRoot, entry.source, 'agent_bundle_workspace_source_invalid');
@@ -1072,6 +1134,16 @@ async function stageAgentBundleWorkspaceFiles(extractedRoot, workspaceDir) {
1072
1134
  await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1073
1135
  await (0, promises_1.copyFile)(source, destination);
1074
1136
  }
1137
+ const overlaySource = taskKind
1138
+ ? resolved.manifest.workspace.taskAgentOverlays?.[taskKind]
1139
+ : null;
1140
+ if (overlaySource) {
1141
+ const agentsPath = resolveWorkspaceFileDestination(workspaceDir, 'AGENTS.md');
1142
+ const overlayPath = normalizeAgentBundlePath(extractedRoot, overlaySource, 'agent_bundle_task_agent_overlay_source_invalid');
1143
+ const base = (0, node_fs_1.readFileSync)(agentsPath, 'utf8').trimEnd();
1144
+ const overlay = (0, node_fs_1.readFileSync)(overlayPath, 'utf8').trim();
1145
+ await (0, promises_1.writeFile)(agentsPath, `${base}\n\n${overlay}\n`, 'utf8');
1146
+ }
1075
1147
  return resolved.pluginRoot;
1076
1148
  }
1077
1149
  async function stageAssignmentPluginBundle(input) {
@@ -1127,7 +1199,7 @@ async function stageAssignmentPluginBundle(input) {
1127
1199
  await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
1128
1200
  await (0, promises_1.writeFile)(destination, Buffer.from(content));
1129
1201
  }
1130
- return await stageAgentBundleWorkspaceFiles(pluginRoot, input.workspaceDir);
1202
+ return await stageAgentBundleWorkspaceFiles(pluginRoot, input.workspaceDir, input.taskKind);
1131
1203
  }
1132
1204
  function resolveDevPluginWorkingTree(input) {
1133
1205
  const workingTree = input.workingTree?.trim();
@@ -1946,8 +2018,22 @@ async function runCodex(input) {
1946
2018
  env: pluginEnv,
1947
2019
  });
1948
2020
  }
2021
+ if (input.enablePlaywrightMcp) {
2022
+ const server = (0, runtime_1.buildPlaywrightMcpCommand)(input.instrumentSurface);
2023
+ await execFileAsync('codex', ['mcp', 'add', 'playwright', '--', server.command, ...server.args], {
2024
+ cwd: input.workspaceDir,
2025
+ env: {
2026
+ ...node_process_1.default.env,
2027
+ HOME: homeDir,
2028
+ CODEX_HOME: codexHomeDir,
2029
+ },
2030
+ });
2031
+ }
1949
2032
  let observedCodexThreadId = null;
1950
2033
  let codexThreadProbe = '';
2034
+ const screenshotCollector = input.enablePlaywrightMcp
2035
+ ? (0, instrument_evidence_1.createWorkerInstrumentScreenshotCollector)(input.workspaceDir)
2036
+ : null;
1951
2037
  const result = await (0, runtime_1.runLoggedProcess)({
1952
2038
  command: 'codex',
1953
2039
  args: (0, runtime_1.buildCodexExecArgs)({
@@ -1981,9 +2067,13 @@ async function runCodex(input) {
1981
2067
  }
1982
2068
  await input.onTranscriptChunks(chunks);
1983
2069
  },
2070
+ ...(screenshotCollector
2071
+ ? { onStdoutText: (text) => screenshotCollector.push(text) > 0 }
2072
+ : {}),
1984
2073
  transcriptFlushIntervalMs: runtime_1.DEFAULT_TRANSCRIPT_FLUSH_INTERVAL_MS,
1985
2074
  onChild: input.onChild,
1986
2075
  });
2076
+ screenshotCollector?.finish();
1987
2077
  observedCodexThreadId = observedCodexThreadId
1988
2078
  ?? (0, runtime_1.extractCodexThreadId)(`${result.stdout}\n${result.stderr}\n${result.outputTail}`);
1989
2079
  if (!observedCodexThreadId) {
@@ -2026,7 +2116,7 @@ async function runClaude(input) {
2026
2116
  if (input.enablePlaywrightMcp) {
2027
2117
  playwrightMcpConfigPath = node_path_1.default.join(input.workspaceDir, '.playdrop', 'claude-run', 'playwright-mcp.json');
2028
2118
  (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(playwrightMcpConfigPath), { recursive: true });
2029
- (0, node_fs_1.writeFileSync)(playwrightMcpConfigPath, `${JSON.stringify((0, runtime_1.buildPlaywrightMcpServerConfig)(), null, 2)}\n`, 'utf8');
2119
+ (0, node_fs_1.writeFileSync)(playwrightMcpConfigPath, `${JSON.stringify((0, runtime_1.buildPlaywrightMcpServerConfig)(input.instrumentSurface), null, 2)}\n`, 'utf8');
2030
2120
  }
2031
2121
  const result = await (0, runtime_1.runLoggedProcess)({
2032
2122
  command: 'claude',
@@ -3381,21 +3471,26 @@ async function writeManagedWorkerLaunchAgent(input) {
3381
3471
  </dict>
3382
3472
  </plist>
3383
3473
  `;
3474
+ const previousPlist = (0, node_fs_1.existsSync)(plistPath) ? (0, node_fs_1.readFileSync)(plistPath, 'utf8') : null;
3475
+ const registrationChanged = previousPlist !== plist;
3384
3476
  await (0, promises_1.writeFile)(plistPath, plist, 'utf8');
3385
- return { label, plistPath };
3477
+ return { label, plistPath, registrationChanged };
3386
3478
  }
3387
- function launchManagedWorker(label, plistPath) {
3388
- const uid = typeof node_process_1.default.getuid === 'function' ? node_process_1.default.getuid() : null;
3479
+ function launchManagedWorker(label, plistPath, options) {
3480
+ const uid = options.uid ?? (typeof node_process_1.default.getuid === 'function' ? node_process_1.default.getuid() : null);
3389
3481
  if (uid === null) {
3390
3482
  throw new Error('worker_setup_launchagent_uid_unavailable');
3391
3483
  }
3484
+ const runCommand = options.runCommand ?? shellProbe_1.runShell;
3392
3485
  const domain = `gui/${uid}`;
3393
- (0, shellProbe_1.runShell)(`launchctl bootout ${shellQuote(`${domain}/${label}`)}`);
3394
- const bootstrap = (0, shellProbe_1.runShell)(`launchctl bootstrap ${shellQuote(domain)} ${shellQuote(plistPath)}`);
3395
- if (bootstrap.exitCode !== 0) {
3396
- throw new Error(`worker_setup_launchagent_start_failed: launchctl bootstrap exited with code ${bootstrap.exitCode}. ${bootstrap.output.slice(0, 300)}`);
3486
+ if (options.registrationChanged) {
3487
+ runCommand(`launchctl bootout ${shellQuote(`${domain}/${label}`)}`);
3488
+ const bootstrap = runCommand(`launchctl bootstrap ${shellQuote(domain)} ${shellQuote(plistPath)}`);
3489
+ if (bootstrap.exitCode !== 0) {
3490
+ throw new Error(`worker_setup_launchagent_start_failed: launchctl bootstrap exited with code ${bootstrap.exitCode}. ${bootstrap.output.slice(0, 300)}`);
3491
+ }
3397
3492
  }
3398
- const kickstart = (0, shellProbe_1.runShell)(`launchctl kickstart -k ${shellQuote(`${domain}/${label}`)}`);
3493
+ const kickstart = runCommand(`launchctl kickstart -k ${shellQuote(`${domain}/${label}`)}`);
3399
3494
  if (kickstart.exitCode !== 0) {
3400
3495
  throw new Error(`worker_setup_launchagent_start_failed: launchctl kickstart exited with code ${kickstart.exitCode}. ${kickstart.output.slice(0, 300)}`);
3401
3496
  }
@@ -3532,7 +3627,7 @@ async function setupWorker(options = {}) {
3532
3627
  }
3533
3628
  verifyNpmGlobalInstallPreconditions();
3534
3629
  const { wrapperPath, policyFile } = await writeManagedWorkerWrapper({ env, workerName, workerKey });
3535
- const { label, plistPath } = await writeManagedWorkerLaunchAgent({ env, workerName, wrapperPath });
3630
+ const { label, plistPath, registrationChanged } = await writeManagedWorkerLaunchAgent({ env, workerName, wrapperPath });
3536
3631
  const status = await resolveWorkerStatusPayload({ env });
3537
3632
  const activeTaskCount = status.server.worker?.activeTaskCount ?? 0;
3538
3633
  const shouldStart = options.start !== false;
@@ -3550,7 +3645,7 @@ async function setupWorker(options = {}) {
3550
3645
  return;
3551
3646
  }
3552
3647
  if (shouldStart || options.restart) {
3553
- launchManagedWorker(label, plistPath);
3648
+ launchManagedWorker(label, plistPath, { registrationChanged });
3554
3649
  }
3555
3650
  (0, output_1.printSuccess)('Managed worker setup complete.', [
3556
3651
  `Wrapper: ${wrapperPath}`,
@@ -4027,11 +4122,12 @@ async function startWorker(options = {}) {
4027
4122
  };
4028
4123
  await stageAssignmentWorkspaceV2(workspaceDir, assignment.workspace);
4029
4124
  const assignmentPluginRoot = devPluginWorkingTree
4030
- ? await stageAgentBundleWorkspaceFiles(devPluginWorkingTree, workspaceDir)
4125
+ ? await stageAgentBundleWorkspaceFiles(devPluginWorkingTree, workspaceDir, task.kind)
4031
4126
  : assignment.plugin
4032
4127
  ? await stageAssignmentPluginBundle({
4033
4128
  workspaceDir,
4034
4129
  pluginBundle: assignment.plugin,
4130
+ taskKind: task.kind,
4035
4131
  })
4036
4132
  : null;
4037
4133
  availableSkillPaths = listPluginSkillPaths(assignmentPluginRoot);
@@ -4118,6 +4214,8 @@ async function startWorker(options = {}) {
4118
4214
  devPort,
4119
4215
  codexModel: codexModel ?? resolveCodexModel(assignment.agent.model),
4120
4216
  taskPluginRoot: assignmentPluginRoot,
4217
+ enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4218
+ instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4121
4219
  imagePaths: imageAttachments.map((attachment) => attachment.path),
4122
4220
  onTranscriptChunks: async (chunks) => {
4123
4221
  await appendTranscriptChunks(chunks);
@@ -4135,6 +4233,7 @@ async function startWorker(options = {}) {
4135
4233
  prompt,
4136
4234
  enableChrome: assignment.agent.browser === 'CHROME',
4137
4235
  enablePlaywrightMcp: assignment.agent.browser === 'PLAYWRIGHT',
4236
+ instrumentSurface: resolveWorkerInstrumentSurface(taskContext),
4138
4237
  envName: env,
4139
4238
  workerUsername: username,
4140
4239
  taskId: task.id,
package/dist/index.js CHANGED
@@ -1021,12 +1021,14 @@ project
1021
1021
  .option('--timeout <seconds>', 'Load-check timeout in seconds')
1022
1022
  .option('--screenshot <path>', 'Screenshot output path')
1023
1023
  .option('--actions <path>', 'JSON action script to dispatch after the game frame is ready')
1024
+ .option('--tape <surface>', 'Run matched idle and catalogue playtest-tape captures on a declared surface')
1024
1025
  .action(async (target, opts = {}) => {
1025
1026
  await (0, check_1.check)(target, {
1026
1027
  app: opts.app,
1027
1028
  timeout: opts.timeout,
1028
1029
  screenshot: opts.screenshot,
1029
1030
  actions: opts.actions,
1031
+ tape: opts.tape,
1030
1032
  });
1031
1033
  });
1032
1034
  const projectCapture = project.command('capture').description('Record game media with the native macOS recorder');
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.12.37",
2
+ "version": "0.12.39",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.12.37",
4
+ "runtimeSdkVersion": "0.12.39",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {