@playdrop/playdrop-cli 0.10.18 → 0.10.19
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/apps/index.js +2 -2
- package/dist/apps/registration.js +1 -0
- package/dist/captureRuntime.d.ts +7 -0
- package/dist/captureRuntime.js +36 -0
- package/dist/commands/captureRemote.d.ts +2 -0
- package/dist/commands/captureRemote.js +42 -2
- package/dist/commands/worker/runtime.d.ts +7 -0
- package/dist/commands/worker/runtime.js +171 -31
- package/dist/commands/worker.d.ts +16 -3
- package/dist/commands/worker.js +459 -115
- package/dist/index.js +18 -3
- package/node_modules/@playdrop/api-client/dist/client.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +28 -1
- package/node_modules/@playdrop/api-client/dist/index.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +10 -0
- package/node_modules/@playdrop/config/client-meta.json +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +64 -3
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +28 -1
- package/package.json +1 -1
package/dist/commands/worker.js
CHANGED
|
@@ -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_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readCodexSandboxMode = exports.readEnvBoolean = exports.extractCodexTokensUsed = exports.extractAgentTokenUsage = exports.buildWorkerChildEnv = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.buildCodexExecArgs = exports.assertWorkerTokenUsageWithinCap = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = void 0;
|
|
6
|
+
exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = exports.WORKER_SESSION_EXPIRED_MESSAGE = exports.runLoggedProcess = exports.readCodexRolloutTokenUsageForThread = exports.readCodexSandboxMode = exports.readEnvBoolean = exports.extractCodexTokensUsed = exports.extractCodexThreadId = exports.extractCodexRolloutTokenUsage = exports.extractAgentTokenUsage = exports.buildWorkerChildEnv = exports.buildClaudePermissionSettings = exports.buildClaudeExecArgs = exports.buildCodexExecArgs = exports.assertWorkerTokenUsageWithinCap = exports.DEFAULT_WORKER_TOKEN_CAP = exports.DEFAULT_CODEX_TIMEOUT_MS = void 0;
|
|
7
7
|
exports.allocateWorkerDevPort = allocateWorkerDevPort;
|
|
8
8
|
exports.isWorkerContextCommandAllowed = isWorkerContextCommandAllowed;
|
|
9
9
|
exports.resolveWorkerExecutionTargetFromRole = resolveWorkerExecutionTargetFromRole;
|
|
@@ -13,6 +13,7 @@ exports.claimBackoffDelayMs = claimBackoffDelayMs;
|
|
|
13
13
|
exports.resolveWorkerHomeDir = resolveWorkerHomeDir;
|
|
14
14
|
exports.hasAgentTaskUploadedArtifact = hasAgentTaskUploadedArtifact;
|
|
15
15
|
exports.appendWorkerTranscriptChunks = appendWorkerTranscriptChunks;
|
|
16
|
+
exports.readTaskNextStepsFile = readTaskNextStepsFile;
|
|
16
17
|
exports.assertTaskUploadResultMatchesContext = assertTaskUploadResultMatchesContext;
|
|
17
18
|
exports.stageAssignmentWorkspaceFiles = stageAssignmentWorkspaceFiles;
|
|
18
19
|
exports.resolvePlaydropPluginRoot = resolvePlaydropPluginRoot;
|
|
@@ -22,6 +23,8 @@ exports.readWorkerTaskState = readWorkerTaskState;
|
|
|
22
23
|
exports.buildWorkerHealthAlertText = buildWorkerHealthAlertText;
|
|
23
24
|
exports.isWorkerAuthFailureError = isWorkerAuthFailureError;
|
|
24
25
|
exports.classifyWorkerEventDrainError = classifyWorkerEventDrainError;
|
|
26
|
+
exports.isWorkerTransientNetworkError = isWorkerTransientNetworkError;
|
|
27
|
+
exports.classifyWorkerHeartbeatError = classifyWorkerHeartbeatError;
|
|
25
28
|
exports.assertWorkerClaimErrorRetryable = assertWorkerClaimErrorRetryable;
|
|
26
29
|
exports.resolvePlaydropAssetRequirementFromText = resolvePlaydropAssetRequirementFromText;
|
|
27
30
|
exports.loadWorkerEnvFile = loadWorkerEnvFile;
|
|
@@ -44,6 +47,7 @@ exports.startWorker = startWorker;
|
|
|
44
47
|
exports.reportTask = reportTask;
|
|
45
48
|
exports.reportCatalogueTask = reportCatalogueTask;
|
|
46
49
|
exports.uploadTask = uploadTask;
|
|
50
|
+
exports.claimSlugTask = claimSlugTask;
|
|
47
51
|
exports.completeTask = completeTask;
|
|
48
52
|
exports.readReviewEvidenceFiles = readReviewEvidenceFiles;
|
|
49
53
|
exports.submitReviewTask = submitReviewTask;
|
|
@@ -75,12 +79,16 @@ Object.defineProperty(exports, "buildClaudeExecArgs", { enumerable: true, get: f
|
|
|
75
79
|
Object.defineProperty(exports, "buildClaudePermissionSettings", { enumerable: true, get: function () { return runtime_2.buildClaudePermissionSettings; } });
|
|
76
80
|
Object.defineProperty(exports, "buildWorkerChildEnv", { enumerable: true, get: function () { return runtime_2.buildWorkerChildEnv; } });
|
|
77
81
|
Object.defineProperty(exports, "extractAgentTokenUsage", { enumerable: true, get: function () { return runtime_2.extractAgentTokenUsage; } });
|
|
82
|
+
Object.defineProperty(exports, "extractCodexRolloutTokenUsage", { enumerable: true, get: function () { return runtime_2.extractCodexRolloutTokenUsage; } });
|
|
83
|
+
Object.defineProperty(exports, "extractCodexThreadId", { enumerable: true, get: function () { return runtime_2.extractCodexThreadId; } });
|
|
78
84
|
Object.defineProperty(exports, "extractCodexTokensUsed", { enumerable: true, get: function () { return runtime_2.extractCodexTokensUsed; } });
|
|
79
85
|
Object.defineProperty(exports, "readEnvBoolean", { enumerable: true, get: function () { return runtime_2.readEnvBoolean; } });
|
|
80
86
|
Object.defineProperty(exports, "readCodexSandboxMode", { enumerable: true, get: function () { return runtime_2.readCodexSandboxMode; } });
|
|
87
|
+
Object.defineProperty(exports, "readCodexRolloutTokenUsageForThread", { enumerable: true, get: function () { return runtime_2.readCodexRolloutTokenUsageForThread; } });
|
|
81
88
|
Object.defineProperty(exports, "runLoggedProcess", { enumerable: true, get: function () { return runtime_2.runLoggedProcess; } });
|
|
82
89
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
83
90
|
const HEARTBEAT_INTERVAL_MS = 10000;
|
|
91
|
+
const TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT = 6;
|
|
84
92
|
const DEFAULT_WORKER_MAX_PARALLEL_TASKS = 5;
|
|
85
93
|
const DEFAULT_WORKER_DEV_PORT_BASE = 8900;
|
|
86
94
|
const CLAIM_BACKOFF_BASE_MS = 1000;
|
|
@@ -92,67 +100,13 @@ const SLACK_API_BASE_URL = 'https://slack.com/api';
|
|
|
92
100
|
const WORKER_WRAPPER_CONTRACT_VERSION = 1;
|
|
93
101
|
const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW'];
|
|
94
102
|
const requireFromWorker = (0, node_module_1.createRequire)(__filename);
|
|
95
|
-
const PLAYDROP_PLUGIN_DAEMON_GAME_CREATION_SKILL_PATH = 'skills/daemon-game-creation/SKILL.md';
|
|
96
|
-
const PLAYDROP_PLUGIN_GAME_IMPROVEMENT_SKILL_PATH = 'skills/game-improvement/SKILL.md';
|
|
97
|
-
const PLAYDROP_PLUGIN_ASSET_DISCOVERY_SKILL_PATH = 'skills/asset-discovery/SKILL.md';
|
|
98
|
-
const PLAYDROP_PLUGIN_ASSET_EXTRACTION_SKILL_PATH = 'skills/asset-extraction-2d/SKILL.md';
|
|
99
|
-
const PLAYDROP_PLUGIN_LISTING_ART_SKILL_PATH = 'skills/listing-art/SKILL.md';
|
|
100
|
-
const PLAYDROP_PLUGIN_GAME_REVIEW_SKILL_PATH = 'skills/game-review/SKILL.md';
|
|
101
|
-
const PLAYDROP_PLUGIN_ASSET_REUSE_REFERENCE_PATH = 'references/asset-reuse.md';
|
|
102
|
-
const PLAYDROP_PLUGIN_ASSETS_AND_GENERATION_REFERENCE_PATH = 'references/assets-and-generation.md';
|
|
103
|
-
const PLAYDROP_PLUGIN_CODE_REUSE_REFERENCE_PATH = 'references/code-reuse.md';
|
|
104
|
-
const PLAYDROP_PLUGIN_EXTRACTOR_PATH = 'scripts/extract-alpha-background-swap.ts';
|
|
105
|
-
const PLAYDROP_PLUGIN_LISTING_ICON_SCRIPT_PATH = 'scripts/compose-listing-icon.ts';
|
|
106
|
-
const PLAYDROP_PLUGIN_LISTING_TITLE_SCRIPT_PATH = 'scripts/compose-listing-title.ts';
|
|
107
103
|
const STAGED_PLAYDROP_PLUGIN_ROOT = '.playdrop/plugin';
|
|
108
|
-
const
|
|
109
|
-
PLAYDROP_PLUGIN_DAEMON_GAME_CREATION_SKILL_PATH,
|
|
110
|
-
PLAYDROP_PLUGIN_GAME_IMPROVEMENT_SKILL_PATH,
|
|
111
|
-
PLAYDROP_PLUGIN_ASSET_DISCOVERY_SKILL_PATH,
|
|
112
|
-
PLAYDROP_PLUGIN_ASSET_EXTRACTION_SKILL_PATH,
|
|
113
|
-
PLAYDROP_PLUGIN_LISTING_ART_SKILL_PATH,
|
|
114
|
-
PLAYDROP_PLUGIN_ASSET_REUSE_REFERENCE_PATH,
|
|
115
|
-
PLAYDROP_PLUGIN_ASSETS_AND_GENERATION_REFERENCE_PATH,
|
|
116
|
-
PLAYDROP_PLUGIN_CODE_REUSE_REFERENCE_PATH,
|
|
117
|
-
PLAYDROP_PLUGIN_EXTRACTOR_PATH,
|
|
118
|
-
PLAYDROP_PLUGIN_LISTING_ICON_SCRIPT_PATH,
|
|
119
|
-
PLAYDROP_PLUGIN_LISTING_TITLE_SCRIPT_PATH,
|
|
120
|
-
];
|
|
121
|
-
const PLAYDROP_PLUGIN_GAME_REVIEW_REFERENCE_FILES = [
|
|
122
|
-
PLAYDROP_PLUGIN_GAME_REVIEW_SKILL_PATH,
|
|
123
|
-
'references/game-review-criteria.md',
|
|
124
|
-
'references/game-review-comparative-method.md',
|
|
125
|
-
'references/game-review-evidence-capture.md',
|
|
126
|
-
'references/game-review-rating-scale.md',
|
|
127
|
-
'references/game-review-score-caps.md',
|
|
128
|
-
'references/game-review-gates.md',
|
|
129
|
-
'references/game-review-gates/concept.md',
|
|
130
|
-
'references/game-review-gates/gameplay-prototype.md',
|
|
131
|
-
'references/game-review-gates/demo.md',
|
|
132
|
-
'references/game-review-gates/vertical-slice.md',
|
|
133
|
-
'references/game-review-gates/first-release.md',
|
|
134
|
-
'references/game-review-gates/mature-live-version.md',
|
|
135
|
-
'references/game-review-outcomes.md',
|
|
136
|
-
'references/game-review-feedback-format.md',
|
|
137
|
-
'references/game-review-dimensions/gameplay-core-loop.md',
|
|
138
|
-
'references/game-review-dimensions/depth-replayability.md',
|
|
139
|
-
'references/game-review-dimensions/controls-input.md',
|
|
140
|
-
'references/game-review-dimensions/ux-usability.md',
|
|
141
|
-
'references/game-review-dimensions/first-time-user-experience.md',
|
|
142
|
-
'references/game-review-dimensions/visuals-art-direction.md',
|
|
143
|
-
'references/game-review-dimensions/audio-feedback.md',
|
|
144
|
-
'references/game-review-dimensions/store-listing-metadata-accuracy.md',
|
|
145
|
-
'references/game-review-dimensions/safety-age-rating-compliance.md',
|
|
146
|
-
'references/game-review-dimensions/performance-stability.md',
|
|
147
|
-
];
|
|
148
|
-
const PLAYDROP_PLUGIN_REFERENCE_FILES = [
|
|
149
|
-
...PLAYDROP_PLUGIN_CREATION_REFERENCE_FILES,
|
|
150
|
-
...PLAYDROP_PLUGIN_GAME_REVIEW_REFERENCE_FILES,
|
|
151
|
-
];
|
|
104
|
+
const PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH = 'playdrop-worker-staging.json';
|
|
152
105
|
exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
|
|
153
106
|
const WORKER_CONTEXT_ALLOWED_COMMANDS = [
|
|
154
107
|
['task', 'report'],
|
|
155
108
|
['task', 'report-catalogue'],
|
|
109
|
+
['task', 'claim-slug'],
|
|
156
110
|
['task', 'upload'],
|
|
157
111
|
['task', 'done'],
|
|
158
112
|
['task', 'submit-review'],
|
|
@@ -175,7 +129,7 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
|
|
|
175
129
|
['ai', 'jobs', 'detail'],
|
|
176
130
|
['ai', 'generations'],
|
|
177
131
|
];
|
|
178
|
-
exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = 'worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task upload/done/fail, assigned
|
|
132
|
+
exports.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/capture, read-only catalogue/documentation lookup, and PlayDrop AI generation are permitted.';
|
|
179
133
|
function allocateWorkerDevPort(input) {
|
|
180
134
|
const basePort = input.basePort ?? DEFAULT_WORKER_DEV_PORT_BASE;
|
|
181
135
|
const maxParallelTasks = input.maxParallelTasks ?? DEFAULT_WORKER_MAX_PARALLEL_TASKS;
|
|
@@ -203,7 +157,7 @@ function isWorkerContextCommandAllowed(argv) {
|
|
|
203
157
|
if (WORKER_CONTEXT_ALLOWED_COMMANDS.some((allowed) => allowed.every((part, index) => argv[index] === part))) {
|
|
204
158
|
return true;
|
|
205
159
|
}
|
|
206
|
-
return
|
|
160
|
+
return isAssignedAppScaffoldCommandAllowed(argv);
|
|
207
161
|
}
|
|
208
162
|
function readWorkerContextForPolicy() {
|
|
209
163
|
try {
|
|
@@ -227,7 +181,7 @@ function normalizeRemixFlagValue(argv) {
|
|
|
227
181
|
}
|
|
228
182
|
return null;
|
|
229
183
|
}
|
|
230
|
-
function
|
|
184
|
+
function isAssignedAppScaffoldCommandAllowed(argv) {
|
|
231
185
|
if (argv[0] !== 'project' || argv[1] !== 'create' || argv[2] !== 'app') {
|
|
232
186
|
return false;
|
|
233
187
|
}
|
|
@@ -239,7 +193,12 @@ function isAssignedRemixScaffoldCommandAllowed(argv) {
|
|
|
239
193
|
if (context?.kind !== 'REMIX_GAME'
|
|
240
194
|
|| !context.outputAppName
|
|
241
195
|
|| requestedAppName !== context.outputAppName) {
|
|
242
|
-
|
|
196
|
+
if (context?.kind !== 'NEW_GAME'
|
|
197
|
+
|| !context.outputAppName
|
|
198
|
+
|| requestedAppName !== context.outputAppName) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return normalizeRemixFlagValue(argv) === null;
|
|
243
202
|
}
|
|
244
203
|
return normalizeRemixFlagValue(argv) === context.remixSourceRef;
|
|
245
204
|
}
|
|
@@ -465,6 +424,9 @@ async function stageWorkerTaskContext(input) {
|
|
|
465
424
|
taskToken: input.taskContext.taskToken,
|
|
466
425
|
}, null, 2), 'utf8');
|
|
467
426
|
}
|
|
427
|
+
async function writeWorkerTaskContextFile(workspaceDir, taskContext) {
|
|
428
|
+
await (0, promises_1.writeFile)(workerTaskContextPath(workspaceDir), JSON.stringify(taskContext, null, 2), 'utf8');
|
|
429
|
+
}
|
|
468
430
|
function findWorkspaceTaskFile(startDir, basename) {
|
|
469
431
|
let current = node_path_1.default.resolve(startDir);
|
|
470
432
|
while (true) {
|
|
@@ -598,6 +560,122 @@ function readTaskUploadResultFile(startDir = node_process_1.default.cwd()) {
|
|
|
598
560
|
}
|
|
599
561
|
return { taskId, appId, appVersionId, appName, version, versionNodeId, creatorUsername };
|
|
600
562
|
}
|
|
563
|
+
const AGENT_TASK_NEXT_STEP_CATEGORY_SET = new Set(types_1.AGENT_TASK_NEXT_STEP_CATEGORIES);
|
|
564
|
+
const AGENT_TASK_CLAIM_SLUG_REGEX = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
|
|
565
|
+
const NEXT_STEPS_SCHEMA_HELP = [
|
|
566
|
+
'Expected next-steps.json format:',
|
|
567
|
+
'{"nextSteps":[{"id":"tune-difficulty","title":"Tune difficulty","description":"Make the opening easier and add a sharper ramp.","prompt":"Tune the game difficulty so the first minute is approachable and the second minute becomes harder.","category":"MECHANICS"}]}',
|
|
568
|
+
`Allowed categories: ${types_1.AGENT_TASK_NEXT_STEP_CATEGORIES.join(', ')}`,
|
|
569
|
+
];
|
|
570
|
+
const NEXT_STEPS_READ_FAILED = Symbol('NEXT_STEPS_READ_FAILED');
|
|
571
|
+
function normalizeTaskClaimSlugAppName(value) {
|
|
572
|
+
const appName = typeof value === 'string' ? value.trim() : '';
|
|
573
|
+
if (!appName || appName.length > 128 || !AGENT_TASK_CLAIM_SLUG_REGEX.test(appName)) {
|
|
574
|
+
throw new Error('invalid_slug_format');
|
|
575
|
+
}
|
|
576
|
+
return appName;
|
|
577
|
+
}
|
|
578
|
+
function normalizeTaskClaimSlugDisplayName(value) {
|
|
579
|
+
const displayName = typeof value === 'string' ? value.trim() : '';
|
|
580
|
+
if (!displayName || displayName.length > 120) {
|
|
581
|
+
throw new Error('invalid_display_name');
|
|
582
|
+
}
|
|
583
|
+
return displayName;
|
|
584
|
+
}
|
|
585
|
+
function readTaskNextStepsFile(filePath) {
|
|
586
|
+
const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
|
|
587
|
+
if (!normalizedPath) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
const resolved = node_path_1.default.resolve(normalizedPath);
|
|
591
|
+
if (!(0, node_fs_1.existsSync)(resolved)) {
|
|
592
|
+
throw new Error('invalid_agent_task_next_steps_file');
|
|
593
|
+
}
|
|
594
|
+
let parsed;
|
|
595
|
+
try {
|
|
596
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(resolved, 'utf8'));
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
throw new Error('invalid_agent_task_next_steps_file');
|
|
600
|
+
}
|
|
601
|
+
let rawSteps = null;
|
|
602
|
+
if (Array.isArray(parsed)) {
|
|
603
|
+
rawSteps = parsed;
|
|
604
|
+
}
|
|
605
|
+
else if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
606
|
+
const object = parsed;
|
|
607
|
+
if (Array.isArray(object['nextSteps'])) {
|
|
608
|
+
rawSteps = object['nextSteps'];
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (!rawSteps) {
|
|
612
|
+
throw new Error('invalid_agent_task_next_steps');
|
|
613
|
+
}
|
|
614
|
+
if (rawSteps.length > 3) {
|
|
615
|
+
throw new Error('invalid_agent_task_next_steps_count');
|
|
616
|
+
}
|
|
617
|
+
const ids = new Set();
|
|
618
|
+
return rawSteps.map((entry) => {
|
|
619
|
+
const object = entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
620
|
+
? entry
|
|
621
|
+
: null;
|
|
622
|
+
if (!object) {
|
|
623
|
+
throw new Error('invalid_agent_task_next_step');
|
|
624
|
+
}
|
|
625
|
+
const id = typeof object['id'] === 'string' ? object['id'].trim() : '';
|
|
626
|
+
if (!id || id.length > 64 || !types_1.AGENT_TASK_NEXT_STEP_ID_REGEX.test(id)) {
|
|
627
|
+
throw new Error('invalid_agent_task_next_step_id');
|
|
628
|
+
}
|
|
629
|
+
if (ids.has(id)) {
|
|
630
|
+
throw new Error('invalid_agent_task_next_step_id_duplicate');
|
|
631
|
+
}
|
|
632
|
+
ids.add(id);
|
|
633
|
+
const title = typeof object['title'] === 'string' ? object['title'].trim() : '';
|
|
634
|
+
if (!title || title.length > 120) {
|
|
635
|
+
throw new Error('invalid_agent_task_next_step_title');
|
|
636
|
+
}
|
|
637
|
+
const description = typeof object['description'] === 'string' ? object['description'].trim() : '';
|
|
638
|
+
if (!description || description.length > 400) {
|
|
639
|
+
throw new Error('invalid_agent_task_next_step_description');
|
|
640
|
+
}
|
|
641
|
+
const prompt = typeof object['prompt'] === 'string' ? object['prompt'].trim() : '';
|
|
642
|
+
if (!prompt || prompt.length > 1200) {
|
|
643
|
+
throw new Error('invalid_agent_task_next_step_prompt');
|
|
644
|
+
}
|
|
645
|
+
const category = typeof object['category'] === 'string' ? object['category'].trim() : '';
|
|
646
|
+
if (!AGENT_TASK_NEXT_STEP_CATEGORY_SET.has(category)) {
|
|
647
|
+
throw new Error('invalid_agent_task_next_step_category');
|
|
648
|
+
}
|
|
649
|
+
return { id, title, description, prompt, category: category };
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
async function recordRejectedNextStepsFile(input) {
|
|
653
|
+
if (!input.ctx) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
await input.ctx.client.workerCreateAgentTaskEvent(input.taskContext.taskId, {
|
|
657
|
+
taskToken: input.taskContext.taskToken,
|
|
658
|
+
kind: 'progress',
|
|
659
|
+
phase: 'next-steps',
|
|
660
|
+
message: `Rejected next-steps.json: ${input.code}`,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
async function readTaskNextStepsFileForCommand(input) {
|
|
664
|
+
try {
|
|
665
|
+
return readTaskNextStepsFile(input.filePath);
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
const code = error instanceof Error ? error.message : String(error);
|
|
669
|
+
await recordRejectedNextStepsFile({
|
|
670
|
+
ctx: input.ctx,
|
|
671
|
+
taskContext: input.taskContext,
|
|
672
|
+
code,
|
|
673
|
+
});
|
|
674
|
+
(0, messages_1.printErrorWithHelp)(code, NEXT_STEPS_SCHEMA_HELP, { command: input.command });
|
|
675
|
+
node_process_1.default.exitCode = 1;
|
|
676
|
+
return NEXT_STEPS_READ_FAILED;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
601
679
|
function assertTaskUploadResultMatchesContext(input) {
|
|
602
680
|
if (input.uploadResult.taskId !== input.taskContext.taskId) {
|
|
603
681
|
throw new Error('task_upload_result_task_mismatch');
|
|
@@ -693,8 +771,97 @@ function candidateVersionDirectories(root) {
|
|
|
693
771
|
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }))
|
|
694
772
|
.map((name) => node_path_1.default.join(root, name));
|
|
695
773
|
}
|
|
696
|
-
function
|
|
697
|
-
|
|
774
|
+
function isDirectory(candidatePath) {
|
|
775
|
+
try {
|
|
776
|
+
return (0, node_fs_1.statSync)(candidatePath).isDirectory();
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function isFile(candidatePath) {
|
|
783
|
+
try {
|
|
784
|
+
return (0, node_fs_1.statSync)(candidatePath).isFile();
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function looksLikePlaydropPluginRoot(pluginRoot) {
|
|
791
|
+
return isDirectory(pluginRoot) && ((0, node_fs_1.existsSync)(node_path_1.default.join(pluginRoot, PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH))
|
|
792
|
+
|| isDirectory(node_path_1.default.join(pluginRoot, 'skills'))
|
|
793
|
+
|| isFile(node_path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json'))
|
|
794
|
+
|| isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))
|
|
795
|
+
|| isFile(node_path_1.default.join(pluginRoot, '.cursor-plugin', 'plugin.json')));
|
|
796
|
+
}
|
|
797
|
+
function normalizePlaydropPluginManifestPath(input) {
|
|
798
|
+
if (typeof input.value !== 'string' || input.value.trim().length <= 0) {
|
|
799
|
+
throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: file paths must be non-empty strings in ${input.manifestPath}`);
|
|
800
|
+
}
|
|
801
|
+
if (input.value.includes('\0') || input.value.includes('\\')) {
|
|
802
|
+
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
|
|
803
|
+
}
|
|
804
|
+
if (node_path_1.default.isAbsolute(input.value) || /^[A-Za-z]:[\\/]/.test(input.value)) {
|
|
805
|
+
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
|
|
806
|
+
}
|
|
807
|
+
const normalized = node_path_1.default.posix.normalize(input.value);
|
|
808
|
+
if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
809
|
+
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${input.kind}:${input.value}`);
|
|
810
|
+
}
|
|
811
|
+
return normalized;
|
|
812
|
+
}
|
|
813
|
+
function readPlaydropPluginStagingManifest(pluginRoot) {
|
|
814
|
+
const resolvedRoot = node_path_1.default.resolve(pluginRoot);
|
|
815
|
+
const manifestPath = node_path_1.default.join(resolvedRoot, PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH);
|
|
816
|
+
if (!isFile(manifestPath)) {
|
|
817
|
+
throw new Error(`playdrop_plugin_staging_manifest_missing: expected ${PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH} under ${resolvedRoot}`);
|
|
818
|
+
}
|
|
819
|
+
let parsed;
|
|
820
|
+
try {
|
|
821
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
throw new Error(`playdrop_plugin_staging_manifest_invalid_json:${manifestPath}:${error instanceof Error ? error.message : String(error)}`);
|
|
825
|
+
}
|
|
826
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
827
|
+
throw new Error(`playdrop_plugin_staging_manifest_invalid:${manifestPath}`);
|
|
828
|
+
}
|
|
829
|
+
const rawManifest = parsed;
|
|
830
|
+
if (rawManifest.version !== 1) {
|
|
831
|
+
throw new Error(`playdrop_plugin_staging_manifest_unsupported_version:${manifestPath}`);
|
|
832
|
+
}
|
|
833
|
+
if (!rawManifest.taskFiles || typeof rawManifest.taskFiles !== 'object' || Array.isArray(rawManifest.taskFiles)) {
|
|
834
|
+
throw new Error(`playdrop_plugin_staging_manifest_incomplete: taskFiles missing in ${manifestPath}`);
|
|
835
|
+
}
|
|
836
|
+
const taskFiles = {};
|
|
837
|
+
const rawTaskFiles = rawManifest.taskFiles;
|
|
838
|
+
for (const kind of WORKER_SUPPORTED_KINDS) {
|
|
839
|
+
const rawFiles = rawTaskFiles[kind];
|
|
840
|
+
if (!Array.isArray(rawFiles) || rawFiles.length <= 0) {
|
|
841
|
+
throw new Error(`playdrop_plugin_staging_manifest_incomplete:${kind}: non-empty taskFiles entry required in ${manifestPath}`);
|
|
842
|
+
}
|
|
843
|
+
const seen = new Set();
|
|
844
|
+
const files = rawFiles.map((value) => normalizePlaydropPluginManifestPath({ manifestPath, kind, value }));
|
|
845
|
+
for (const relativePath of files) {
|
|
846
|
+
if (seen.has(relativePath)) {
|
|
847
|
+
throw new Error(`playdrop_plugin_staging_manifest_duplicate_file:${kind}:${relativePath}`);
|
|
848
|
+
}
|
|
849
|
+
seen.add(relativePath);
|
|
850
|
+
const source = node_path_1.default.resolve(resolvedRoot, ...relativePath.split('/'));
|
|
851
|
+
const relativeToRoot = node_path_1.default.relative(resolvedRoot, source);
|
|
852
|
+
if (relativeToRoot.startsWith('..') || node_path_1.default.isAbsolute(relativeToRoot)) {
|
|
853
|
+
throw new Error(`playdrop_plugin_staging_manifest_invalid_path:${kind}:${relativePath}`);
|
|
854
|
+
}
|
|
855
|
+
if (!isFile(source)) {
|
|
856
|
+
throw new Error(`playdrop_plugin_staging_manifest_file_missing:${kind}:${relativePath}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
taskFiles[kind] = files;
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
version: 1,
|
|
863
|
+
taskFiles,
|
|
864
|
+
};
|
|
698
865
|
}
|
|
699
866
|
function resolvePlaydropPluginRoot(input = {}) {
|
|
700
867
|
const homeDir = input.homeDir ?? node_os_1.default.homedir();
|
|
@@ -702,9 +869,7 @@ function resolvePlaydropPluginRoot(input = {}) {
|
|
|
702
869
|
const explicitRoot = env.PLAYDROP_WORKER_PLAYDROP_PLUGIN_DIR?.trim();
|
|
703
870
|
if (explicitRoot) {
|
|
704
871
|
const resolved = node_path_1.default.resolve(explicitRoot);
|
|
705
|
-
|
|
706
|
-
throw new Error(`playdrop_plugin_reference_missing: expected PlayDrop plugin skills and scripts under ${resolved}`);
|
|
707
|
-
}
|
|
872
|
+
readPlaydropPluginStagingManifest(resolved);
|
|
708
873
|
return resolved;
|
|
709
874
|
}
|
|
710
875
|
const candidates = [
|
|
@@ -716,27 +881,22 @@ function resolvePlaydropPluginRoot(input = {}) {
|
|
|
716
881
|
...candidateVersionDirectories(node_path_1.default.join(homeDir, '.codex', 'plugins', 'cache', 'playdrop', 'playdrop')),
|
|
717
882
|
...candidateVersionDirectories(node_path_1.default.join(homeDir, '.codex', 'plugins', 'cache', 'olivier-local-plugins', 'playdrop')),
|
|
718
883
|
];
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
884
|
+
for (const candidate of candidates) {
|
|
885
|
+
if (!looksLikePlaydropPluginRoot(candidate)) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
readPlaydropPluginStagingManifest(candidate);
|
|
889
|
+
return candidate;
|
|
722
890
|
}
|
|
723
|
-
|
|
891
|
+
throw new Error(`playdrop_plugin_staging_manifest_missing: install or update the PlayDrop plugin so ${PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH} is available.`);
|
|
724
892
|
}
|
|
725
893
|
async function stagePlaydropPluginReferences(input) {
|
|
726
894
|
const pluginRoot = node_path_1.default.resolve(input.pluginRoot);
|
|
727
|
-
if (!
|
|
728
|
-
throw new Error(`playdrop_plugin_reference_missing: expected required PlayDrop plugin skills and scripts under ${pluginRoot}`);
|
|
729
|
-
}
|
|
730
|
-
let referenceFiles;
|
|
731
|
-
if (input.kind === 'GAME_REVIEW') {
|
|
732
|
-
referenceFiles = PLAYDROP_PLUGIN_GAME_REVIEW_REFERENCE_FILES;
|
|
733
|
-
}
|
|
734
|
-
else if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME' || input.kind === 'GAME_UPDATE') {
|
|
735
|
-
referenceFiles = PLAYDROP_PLUGIN_CREATION_REFERENCE_FILES;
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
895
|
+
if (!WORKER_SUPPORTED_KINDS.includes(input.kind)) {
|
|
738
896
|
throw new Error(`unsupported_agent_task_kind:${input.kind}`);
|
|
739
897
|
}
|
|
898
|
+
const manifest = readPlaydropPluginStagingManifest(pluginRoot);
|
|
899
|
+
const referenceFiles = manifest.taskFiles[input.kind];
|
|
740
900
|
const staged = [];
|
|
741
901
|
for (const relativePath of referenceFiles) {
|
|
742
902
|
const destinationRelativePath = node_path_1.default.join(STAGED_PLAYDROP_PLUGIN_ROOT, relativePath);
|
|
@@ -930,6 +1090,38 @@ function classifyWorkerEventDrainError(error) {
|
|
|
930
1090
|
}
|
|
931
1091
|
return null;
|
|
932
1092
|
}
|
|
1093
|
+
function readErrorCauseCode(error) {
|
|
1094
|
+
const cause = error instanceof Error ? error.cause : null;
|
|
1095
|
+
if (cause && typeof cause === 'object' && 'code' in cause && typeof cause.code === 'string') {
|
|
1096
|
+
return cause.code;
|
|
1097
|
+
}
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
function isWorkerTransientNetworkError(error) {
|
|
1101
|
+
if (!(error instanceof Error)) {
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
const message = error.message.toLowerCase();
|
|
1105
|
+
if (message.includes('fetch failed') || message.includes('socket hang up')) {
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
const causeCode = readErrorCauseCode(error);
|
|
1109
|
+
return causeCode
|
|
1110
|
+
? ['ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT'].includes(causeCode)
|
|
1111
|
+
: false;
|
|
1112
|
+
}
|
|
1113
|
+
function classifyWorkerHeartbeatError(error) {
|
|
1114
|
+
if (isWorkerAuthFailureError(error)) {
|
|
1115
|
+
return 'session_expired';
|
|
1116
|
+
}
|
|
1117
|
+
if (isLeaseInvalidError(error)) {
|
|
1118
|
+
return 'lease_invalid';
|
|
1119
|
+
}
|
|
1120
|
+
if (isWorkerTransientNetworkError(error)) {
|
|
1121
|
+
return 'transient_network';
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
933
1125
|
// Throws for claim errors that must stop the worker; returns for transient
|
|
934
1126
|
// errors the claim loop may retry with backoff.
|
|
935
1127
|
function assertWorkerClaimErrorRetryable(error) {
|
|
@@ -1349,10 +1541,15 @@ function normalizeQueuedWorkerEvent(value) {
|
|
|
1349
1541
|
if (!kind) {
|
|
1350
1542
|
throw new Error('agent_task_worker_event_kind_invalid');
|
|
1351
1543
|
}
|
|
1352
|
-
const message = typeof object.message === 'string'
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1544
|
+
const message = typeof object.message === 'string' && object.message.trim()
|
|
1545
|
+
? object.message.trim()
|
|
1546
|
+
: null;
|
|
1547
|
+
const done = typeof object.done === 'string' && object.done.trim()
|
|
1548
|
+
? object.done.trim()
|
|
1549
|
+
: null;
|
|
1550
|
+
const current = typeof object.current === 'string' && object.current.trim()
|
|
1551
|
+
? object.current.trim()
|
|
1552
|
+
: null;
|
|
1356
1553
|
const phase = typeof object.phase === 'string' && object.phase.trim()
|
|
1357
1554
|
? object.phase.trim()
|
|
1358
1555
|
: null;
|
|
@@ -1365,10 +1562,16 @@ function normalizeQueuedWorkerEvent(value) {
|
|
|
1365
1562
|
if (kind === 'catalogue_preview' && !payload) {
|
|
1366
1563
|
throw new Error('agent_task_worker_event_payload_missing');
|
|
1367
1564
|
}
|
|
1565
|
+
if (kind === 'catalogue_preview' && !message) {
|
|
1566
|
+
throw new Error('agent_task_worker_event_message_missing');
|
|
1567
|
+
}
|
|
1568
|
+
if (kind === 'progress' && !message && !done && !current) {
|
|
1569
|
+
throw new Error('agent_task_worker_event_message_missing');
|
|
1570
|
+
}
|
|
1368
1571
|
if (kind === 'progress') {
|
|
1369
|
-
return { kind, phase, message, pct };
|
|
1572
|
+
return { kind, phase, message, done, current, pct };
|
|
1370
1573
|
}
|
|
1371
|
-
return { kind, phase, message, pct, payload };
|
|
1574
|
+
return { kind, phase, message, done, current, pct, payload };
|
|
1372
1575
|
}
|
|
1373
1576
|
async function drainWorkerEventQueue(input) {
|
|
1374
1577
|
if (!(0, node_fs_1.existsSync)(input.eventDir)) {
|
|
@@ -1386,8 +1589,10 @@ async function drainWorkerEventQueue(input) {
|
|
|
1386
1589
|
leaseToken: input.leaseToken,
|
|
1387
1590
|
kind: event.kind,
|
|
1388
1591
|
phase: event.phase,
|
|
1389
|
-
message: event.message,
|
|
1390
|
-
|
|
1592
|
+
...(event.message ? { message: event.message } : {}),
|
|
1593
|
+
...(event.done ? { done: event.done } : {}),
|
|
1594
|
+
...(event.current ? { current: event.current } : {}),
|
|
1595
|
+
...(event.pct !== null && event.pct !== undefined ? { pct: event.pct } : {}),
|
|
1391
1596
|
...(event.payload ? { payload: event.payload } : {}),
|
|
1392
1597
|
});
|
|
1393
1598
|
(0, node_fs_1.rmSync)(filePath, { force: true });
|
|
@@ -1443,7 +1648,9 @@ function resolveClaudeRuntimeModel(model) {
|
|
|
1443
1648
|
return model;
|
|
1444
1649
|
}
|
|
1445
1650
|
async function runCodex(input) {
|
|
1446
|
-
|
|
1651
|
+
let observedCodexThreadId = null;
|
|
1652
|
+
let codexThreadProbe = '';
|
|
1653
|
+
const result = await (0, runtime_1.runLoggedProcess)({
|
|
1447
1654
|
command: 'codex',
|
|
1448
1655
|
args: (0, runtime_1.buildCodexExecArgs)({
|
|
1449
1656
|
workspaceDir: input.workspaceDir,
|
|
@@ -1464,10 +1671,41 @@ async function runCodex(input) {
|
|
|
1464
1671
|
stdin: input.prompt,
|
|
1465
1672
|
timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CODEX_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
|
|
1466
1673
|
maxOutputChars: runtime_1.DEFAULT_CODEX_LOG_TAIL_CHARS,
|
|
1467
|
-
onTranscriptChunks:
|
|
1674
|
+
onTranscriptChunks: async (chunks) => {
|
|
1675
|
+
if (!observedCodexThreadId) {
|
|
1676
|
+
codexThreadProbe = (codexThreadProbe + chunks.map((chunk) => chunk.content).join('')).slice(-64000);
|
|
1677
|
+
observedCodexThreadId = (0, runtime_1.extractCodexThreadId)(codexThreadProbe);
|
|
1678
|
+
}
|
|
1679
|
+
await input.onTranscriptChunks(chunks);
|
|
1680
|
+
},
|
|
1468
1681
|
transcriptFlushIntervalMs: runtime_1.DEFAULT_TRANSCRIPT_FLUSH_INTERVAL_MS,
|
|
1469
1682
|
onChild: input.onChild,
|
|
1470
1683
|
});
|
|
1684
|
+
observedCodexThreadId = observedCodexThreadId
|
|
1685
|
+
?? (0, runtime_1.extractCodexThreadId)(`${result.stdout}\n${result.stderr}\n${result.outputTail}`);
|
|
1686
|
+
if (!observedCodexThreadId) {
|
|
1687
|
+
if (result.tokenUsage.usageParseError) {
|
|
1688
|
+
console.warn('Codex thread id was not observed; rollout token usage could not be read.');
|
|
1689
|
+
}
|
|
1690
|
+
return result;
|
|
1691
|
+
}
|
|
1692
|
+
const rolloutUsage = await (0, runtime_1.readCodexRolloutTokenUsageForThread)(observedCodexThreadId);
|
|
1693
|
+
if (rolloutUsage.usageParseError || rolloutUsage.totalTokens === null) {
|
|
1694
|
+
if (result.tokenUsage.usageParseError) {
|
|
1695
|
+
console.warn(`Codex rollout token usage could not be read for thread ${observedCodexThreadId}: ${rolloutUsage.usageParseError ?? 'unknown_error'}.`);
|
|
1696
|
+
}
|
|
1697
|
+
return result;
|
|
1698
|
+
}
|
|
1699
|
+
if (!result.tokenUsage.usageParseError
|
|
1700
|
+
&& result.tokenUsage.totalTokens !== null
|
|
1701
|
+
&& result.tokenUsage.totalTokens !== rolloutUsage.totalTokens) {
|
|
1702
|
+
console.warn(`Codex stream token usage (${result.tokenUsage.totalTokens}) differed from rollout token usage (${rolloutUsage.totalTokens}); using rollout usage.`);
|
|
1703
|
+
}
|
|
1704
|
+
return {
|
|
1705
|
+
...result,
|
|
1706
|
+
tokenUsage: rolloutUsage,
|
|
1707
|
+
tokensUsed: rolloutUsage.totalTokens,
|
|
1708
|
+
};
|
|
1471
1709
|
}
|
|
1472
1710
|
async function runClaude(input) {
|
|
1473
1711
|
const denyReadRoots = resolveClaudeDenyReadRoots(input.workspaceDir);
|
|
@@ -2849,6 +3087,7 @@ async function startWorker(options = {}) {
|
|
|
2849
3087
|
let eventDrainTimer = null;
|
|
2850
3088
|
let eventDrainPromise = Promise.resolve();
|
|
2851
3089
|
let fenced = false;
|
|
3090
|
+
let heartbeatObservedFinalizedTask = false;
|
|
2852
3091
|
let retainWorkspace = false;
|
|
2853
3092
|
let workspaceDir = null;
|
|
2854
3093
|
let availableSkillPaths = [];
|
|
@@ -2861,6 +3100,7 @@ async function startWorker(options = {}) {
|
|
|
2861
3100
|
let agentCompletedAt = null;
|
|
2862
3101
|
let agentResult = null;
|
|
2863
3102
|
let telemetryReported = false;
|
|
3103
|
+
let heartbeatTransientFailures = 0;
|
|
2864
3104
|
const reportTelemetry = async (status, setupError) => {
|
|
2865
3105
|
if (!assignment) {
|
|
2866
3106
|
return;
|
|
@@ -2974,19 +3214,49 @@ async function startWorker(options = {}) {
|
|
|
2974
3214
|
heartbeatTimer = setInterval(() => {
|
|
2975
3215
|
client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
|
|
2976
3216
|
.then((heartbeat) => {
|
|
3217
|
+
heartbeatTransientFailures = 0;
|
|
2977
3218
|
if (heartbeat.action === 'abort') {
|
|
2978
3219
|
fenced = true;
|
|
2979
3220
|
activeTerminators.get(task.id)?.();
|
|
2980
3221
|
}
|
|
2981
3222
|
})
|
|
2982
3223
|
.catch((error) => {
|
|
2983
|
-
|
|
3224
|
+
const classification = classifyWorkerHeartbeatError(error);
|
|
3225
|
+
if (classification === 'session_expired') {
|
|
2984
3226
|
handleSessionExpiry();
|
|
2985
3227
|
return;
|
|
2986
3228
|
}
|
|
2987
|
-
if (
|
|
2988
|
-
|
|
2989
|
-
|
|
3229
|
+
if (classification === 'lease_invalid') {
|
|
3230
|
+
void (async () => {
|
|
3231
|
+
const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
|
|
3232
|
+
if (refreshed?.task.status === 'DONE') {
|
|
3233
|
+
fenced = true;
|
|
3234
|
+
if (!heartbeatObservedFinalizedTask) {
|
|
3235
|
+
heartbeatObservedFinalizedTask = true;
|
|
3236
|
+
console.error(`Agent task ${task.id} lease ended after task completed as DONE; waiting for agent exit.`);
|
|
3237
|
+
}
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
if (refreshed && refreshed.task.status !== 'RUNNING' && refreshed.task.status !== 'QUEUED') {
|
|
3241
|
+
console.error(`Agent task ${task.id} lease ended after task finalized as ${refreshed.task.status}; terminating agent child.`);
|
|
3242
|
+
}
|
|
3243
|
+
fenced = true;
|
|
3244
|
+
activeTerminators.get(task.id)?.();
|
|
3245
|
+
})().catch((heartbeatError) => {
|
|
3246
|
+
fenced = true;
|
|
3247
|
+
activeTerminators.get(task.id)?.();
|
|
3248
|
+
console.error(`Agent task lease check failed: ${heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError)}`);
|
|
3249
|
+
});
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
if (classification === 'transient_network') {
|
|
3253
|
+
heartbeatTransientFailures += 1;
|
|
3254
|
+
console.error(`Agent task heartbeat transient failure ${heartbeatTransientFailures}/${TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3255
|
+
if (heartbeatTransientFailures >= TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT) {
|
|
3256
|
+
fenced = true;
|
|
3257
|
+
activeTerminators.get(task.id)?.();
|
|
3258
|
+
console.error(`Agent task ${task.id} heartbeat could not reach the API; terminating agent child.`);
|
|
3259
|
+
}
|
|
2990
3260
|
return;
|
|
2991
3261
|
}
|
|
2992
3262
|
console.error(`Agent task heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -3415,9 +3685,11 @@ async function startWorker(options = {}) {
|
|
|
3415
3685
|
}, { env: options.env });
|
|
3416
3686
|
}
|
|
3417
3687
|
async function reportTask(options) {
|
|
3418
|
-
const message = options.message?.trim();
|
|
3419
|
-
|
|
3420
|
-
|
|
3688
|
+
const message = options.message?.trim() || null;
|
|
3689
|
+
const done = options.done?.trim() || null;
|
|
3690
|
+
const current = options.current?.trim() || null;
|
|
3691
|
+
if (!message && !done && !current) {
|
|
3692
|
+
(0, messages_1.printErrorWithHelp)('Task report requires a message, done, or current value.', ['Example: playdrop task report --phase build --done "Built the first loop" --current "Tuning controls"'], { command: 'task report' });
|
|
3421
3693
|
node_process_1.default.exitCode = 1;
|
|
3422
3694
|
return;
|
|
3423
3695
|
}
|
|
@@ -3425,7 +3697,14 @@ async function reportTask(options) {
|
|
|
3425
3697
|
const kind = normalizeTaskEventKind(options.kind);
|
|
3426
3698
|
const phase = options.phase?.trim() || null;
|
|
3427
3699
|
if (resolveWorkerEventQueueDir()) {
|
|
3428
|
-
enqueueWorkerContextEvent({
|
|
3700
|
+
enqueueWorkerContextEvent({
|
|
3701
|
+
kind,
|
|
3702
|
+
phase,
|
|
3703
|
+
...(message ? { message } : {}),
|
|
3704
|
+
...(done ? { done } : {}),
|
|
3705
|
+
...(current ? { current } : {}),
|
|
3706
|
+
...(pct !== null ? { pct } : {}),
|
|
3707
|
+
});
|
|
3429
3708
|
(0, output_1.printSuccess)('Task progress queued.');
|
|
3430
3709
|
return;
|
|
3431
3710
|
}
|
|
@@ -3522,6 +3801,34 @@ async function uploadTask(options = {}) {
|
|
|
3522
3801
|
}
|
|
3523
3802
|
(0, output_1.printSuccess)(`Uploaded ${published.creatorUsername}/${published.appName} version ${published.version}. Run "playdrop task done" to close the task.`);
|
|
3524
3803
|
}
|
|
3804
|
+
async function claimSlugTask(options) {
|
|
3805
|
+
const taskContext = readTaskContextFile();
|
|
3806
|
+
if (taskContext.kind !== 'NEW_GAME' && taskContext.kind !== 'REMIX_GAME') {
|
|
3807
|
+
throw new Error('task_claim_slug_not_allowed_for_kind');
|
|
3808
|
+
}
|
|
3809
|
+
const appName = normalizeTaskClaimSlugAppName(options.appName);
|
|
3810
|
+
const displayName = normalizeTaskClaimSlugDisplayName(options.displayName);
|
|
3811
|
+
const ctx = await resolveTaskCommandContext('task claim-slug', options.env, taskContext);
|
|
3812
|
+
if (!ctx) {
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
const response = await ctx.client.workerClaimAgentTaskSlug(taskContext.taskId, {
|
|
3816
|
+
appName,
|
|
3817
|
+
displayName,
|
|
3818
|
+
});
|
|
3819
|
+
const claimedAppName = typeof response.appName === 'string' && response.appName.trim()
|
|
3820
|
+
? response.appName.trim()
|
|
3821
|
+
: '';
|
|
3822
|
+
if (claimedAppName !== appName) {
|
|
3823
|
+
throw new Error('task_claim_slug_response_mismatch');
|
|
3824
|
+
}
|
|
3825
|
+
const workspaceDir = resolveTaskWorkspaceDir();
|
|
3826
|
+
await writeWorkerTaskContextFile(workspaceDir, {
|
|
3827
|
+
...taskContext,
|
|
3828
|
+
outputAppName: claimedAppName,
|
|
3829
|
+
});
|
|
3830
|
+
(0, output_1.printSuccess)(`Claimed app slug "${claimedAppName}" for ${displayName}.`);
|
|
3831
|
+
}
|
|
3525
3832
|
async function completeTask(options) {
|
|
3526
3833
|
const taskContext = readTaskContextFile();
|
|
3527
3834
|
if (taskContext.kind === 'GAME_REVIEW') {
|
|
@@ -3540,21 +3847,48 @@ async function completeTask(options) {
|
|
|
3540
3847
|
if (uploadResult.taskId !== taskContext.taskId) {
|
|
3541
3848
|
throw new Error('task_done_upload_result_task_mismatch');
|
|
3542
3849
|
}
|
|
3543
|
-
await
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
appId: uploadResult.appId,
|
|
3549
|
-
appVersionId: uploadResult.appVersionId,
|
|
3550
|
-
version: uploadResult.version,
|
|
3551
|
-
appName: uploadResult.appName,
|
|
3552
|
-
versionNodeId: uploadResult.versionNodeId,
|
|
3553
|
-
creatorUsername: uploadResult.creatorUsername,
|
|
3554
|
-
completedBy: 'agent',
|
|
3555
|
-
},
|
|
3850
|
+
const nextSteps = await readTaskNextStepsFileForCommand({
|
|
3851
|
+
filePath: options.nextSteps,
|
|
3852
|
+
command: 'task done',
|
|
3853
|
+
ctx,
|
|
3854
|
+
taskContext,
|
|
3556
3855
|
});
|
|
3557
|
-
|
|
3856
|
+
if (nextSteps === NEXT_STEPS_READ_FAILED) {
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
try {
|
|
3860
|
+
await ctx.client.workerCompleteAgentTask(taskContext.taskId, {
|
|
3861
|
+
taskToken: taskContext.taskToken,
|
|
3862
|
+
appId,
|
|
3863
|
+
appVersionId,
|
|
3864
|
+
result: {
|
|
3865
|
+
appId: uploadResult.appId,
|
|
3866
|
+
appVersionId: uploadResult.appVersionId,
|
|
3867
|
+
version: uploadResult.version,
|
|
3868
|
+
appName: uploadResult.appName,
|
|
3869
|
+
versionNodeId: uploadResult.versionNodeId,
|
|
3870
|
+
creatorUsername: uploadResult.creatorUsername,
|
|
3871
|
+
completedBy: 'agent',
|
|
3872
|
+
},
|
|
3873
|
+
...(nextSteps !== undefined ? { nextSteps } : {}),
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
catch (error) {
|
|
3877
|
+
if (error instanceof types_1.ApiError && error.status === 409 && error.code === 'remix_relation_missing') {
|
|
3878
|
+
await ctx.client.workerFailAgentTask(taskContext.taskId, {
|
|
3879
|
+
taskToken: taskContext.taskToken,
|
|
3880
|
+
error: 'remix_relation_missing',
|
|
3881
|
+
result: {
|
|
3882
|
+
appId: uploadResult.appId,
|
|
3883
|
+
appVersionId: uploadResult.appVersionId,
|
|
3884
|
+
appName: uploadResult.appName,
|
|
3885
|
+
version: uploadResult.version,
|
|
3886
|
+
},
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
throw error;
|
|
3890
|
+
}
|
|
3891
|
+
(0, output_1.printSuccess)('Task marked done. Send one short final status sentence now, then stop.');
|
|
3558
3892
|
}
|
|
3559
3893
|
function normalizeReviewSubmitState(value) {
|
|
3560
3894
|
const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
@@ -3683,15 +4017,25 @@ async function submitReviewTask(options) {
|
|
|
3683
4017
|
if (!ctx) {
|
|
3684
4018
|
throw new Error('task_submit_review_auth_required');
|
|
3685
4019
|
}
|
|
4020
|
+
const nextSteps = await readTaskNextStepsFileForCommand({
|
|
4021
|
+
filePath: options.nextSteps,
|
|
4022
|
+
command: 'task submit-review',
|
|
4023
|
+
ctx,
|
|
4024
|
+
taskContext,
|
|
4025
|
+
});
|
|
4026
|
+
if (nextSteps === NEXT_STEPS_READ_FAILED) {
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
3686
4029
|
await ctx.client.workerSubmitAgentTaskReview(taskContext.taskId, {
|
|
3687
4030
|
taskToken: taskContext.taskToken,
|
|
3688
4031
|
appVersionId: taskContext.reviewAppVersionId ?? 0,
|
|
3689
4032
|
reviewState: state,
|
|
3690
4033
|
reviewMessage,
|
|
3691
4034
|
creatorFeedback,
|
|
4035
|
+
...(nextSteps !== undefined ? { nextSteps } : {}),
|
|
3692
4036
|
evidenceFiles,
|
|
3693
4037
|
});
|
|
3694
|
-
(0, output_1.printSuccess)('Review submitted.');
|
|
4038
|
+
(0, output_1.printSuccess)('Review submitted. Send one short final status sentence now, then stop.');
|
|
3695
4039
|
}
|
|
3696
4040
|
async function failTask(options) {
|
|
3697
4041
|
const message = options.message?.trim();
|
|
@@ -3710,5 +4054,5 @@ async function failTask(options) {
|
|
|
3710
4054
|
error: message,
|
|
3711
4055
|
result: { failedBy: 'agent' },
|
|
3712
4056
|
});
|
|
3713
|
-
(0, output_1.printSuccess)('Task marked failed.');
|
|
4057
|
+
(0, output_1.printSuccess)('Task marked failed. Send one short final status sentence now, then stop.');
|
|
3714
4058
|
}
|