@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.
@@ -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 PLAYDROP_PLUGIN_CREATION_REFERENCE_FILES = [
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 remix scaffolding, project validation/build/dev/capture, read-only catalogue/documentation lookup, and PlayDrop AI generation are permitted.';
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 isAssignedRemixScaffoldCommandAllowed(argv);
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 isAssignedRemixScaffoldCommandAllowed(argv) {
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
- return false;
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 hasPlaydropPluginReferences(pluginRoot) {
697
- return PLAYDROP_PLUGIN_REFERENCE_FILES.every((relativePath) => (0, node_fs_1.existsSync)(node_path_1.default.join(pluginRoot, relativePath)));
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
- if (!hasPlaydropPluginReferences(resolved)) {
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 pluginRoot = candidates.find(hasPlaydropPluginReferences);
720
- if (!pluginRoot) {
721
- throw new Error('playdrop_plugin_reference_missing: install or update the PlayDrop plugin so the required skills and scripts are available.');
884
+ for (const candidate of candidates) {
885
+ if (!looksLikePlaydropPluginRoot(candidate)) {
886
+ continue;
887
+ }
888
+ readPlaydropPluginStagingManifest(candidate);
889
+ return candidate;
722
890
  }
723
- return pluginRoot;
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 (!hasPlaydropPluginReferences(pluginRoot)) {
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' ? object.message.trim() : '';
1353
- if (!message) {
1354
- throw new Error('agent_task_worker_event_message_missing');
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
- pct: event.pct,
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
- return await (0, runtime_1.runLoggedProcess)({
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: input.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
- if (isWorkerAuthFailureError(error)) {
3224
+ const classification = classifyWorkerHeartbeatError(error);
3225
+ if (classification === 'session_expired') {
2984
3226
  handleSessionExpiry();
2985
3227
  return;
2986
3228
  }
2987
- if (isLeaseInvalidError(error)) {
2988
- fenced = true;
2989
- activeTerminators.get(task.id)?.();
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
- if (!message) {
3420
- (0, messages_1.printErrorWithHelp)('Task report requires a message.', ['Example: playdrop task report --phase build --pct 25 -m "Building first loop"'], { command: 'task report' });
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({ kind, phase, message, pct });
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 ctx.client.workerCompleteAgentTask(taskContext.taskId, {
3544
- taskToken: taskContext.taskToken,
3545
- appId,
3546
- appVersionId,
3547
- result: {
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
- (0, output_1.printSuccess)('Task marked done.');
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
  }