@playcraft/cli 0.1.66 → 0.1.67

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 (101) hide show
  1. package/dist/assets/client.js +4 -2
  2. package/dist/assets/messages.js +2 -0
  3. package/dist/audio-source/messages.js +35 -0
  4. package/dist/commands/assets.js +1 -1
  5. package/dist/commands/audio-source.js +109 -0
  6. package/dist/commands/audio.js +11 -4
  7. package/dist/commands/build.js +4 -2
  8. package/dist/commands/tools-context.js +46 -0
  9. package/dist/commands/tools.js +2 -0
  10. package/dist/project-skills/builtin-import.js +69 -22
  11. package/dist/project-skills/commands.js +11 -2
  12. package/dist/project-skills/lifecycle.js +1 -1
  13. package/dist/project-skills/local-store.js +90 -31
  14. package/dist/project-skills/messages.js +16 -0
  15. package/dist/project-skills/migrate-lock.js +166 -0
  16. package/dist/project-skills/remote-cache.js +36 -5
  17. package/dist/utils/agent-api-client.js +18 -1
  18. package/dist/utils/agent-generation-provenance.js +8 -2
  19. package/dist/utils/audio-export.js +249 -0
  20. package/dist/utils/audio-processor.js +22 -99
  21. package/dist/utils/execution-context-bridge.js +28 -0
  22. package/dist/workspace-runtime/batch/control.js +13 -2
  23. package/dist/workspace-runtime/batch/generate.js +43 -2
  24. package/dist/workspace-runtime/batch/prepare.js +4 -0
  25. package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
  26. package/dist/workspace-runtime/batch/source-capture.js +6 -8
  27. package/dist/workspace-runtime/codex/app-server.js +26 -2
  28. package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
  29. package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
  30. package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
  31. package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
  32. package/dist/workspace-runtime/codex/model-broker.js +15 -2
  33. package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
  34. package/dist/workspace-runtime/previews/finalize.js +1 -1
  35. package/dist/workspace-runtime/previews/output.js +4 -1
  36. package/dist/workspace-runtime/previews/publish.js +5 -2
  37. package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
  38. package/dist/workspace-runtime/serve.js +6 -0
  39. package/dist/workspace-runtime/server/accept-operation.js +85 -13
  40. package/dist/workspace-runtime/server/audio-assets.js +211 -0
  41. package/dist/workspace-runtime/server/audio-export.js +136 -0
  42. package/dist/workspace-runtime/server/directory-access.js +222 -67
  43. package/dist/workspace-runtime/server/dispatch.js +252 -29
  44. package/dist/workspace-runtime/server/http.js +97 -3
  45. package/dist/workspace-runtime/server/operation-record.js +1 -1
  46. package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
  47. package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
  48. package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
  49. package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
  50. package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +76 -0
  51. package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
  52. package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
  53. package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
  54. package/dist/workspace-runtime/workspaces/audio-assets/service.js +327 -0
  55. package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
  56. package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
  57. package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
  58. package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
  59. package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
  60. package/dist/workspace-runtime/workspaces/files.js +3 -5
  61. package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
  62. package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
  63. package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
  64. package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
  65. package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
  66. package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
  67. package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
  68. package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
  69. package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
  70. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
  71. package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
  72. package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
  73. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
  74. package/dist/workspace-runtime/workspaces/skills.js +48 -13
  75. package/package.json +6 -4
  76. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  77. package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
  78. package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
  79. package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
  80. package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
  81. package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
  82. package/project-template/vite.config.ts +1 -1
  83. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  84. package/project-template-v2/.playcraft/skills.lock.json +9 -2
  85. package/project-template-v2/vite.config.ts +1 -1
  86. package/project-templates/empty/AGENTS.md +2 -0
  87. package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  88. package/project-templates/phaser/vite.config.ts +1 -1
  89. package/project-templates/playable-template-phaser/vite.config.ts +1 -1
  90. package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +74 -4
  91. package/project-templates/threejs/vite.config.ts +1 -1
  92. package/templates/preview-runtime/v1/README.md +12 -0
  93. package/templates/preview-runtime/v1/build-preview.mjs +17 -8
  94. package/templates/preview-runtime/v1/manifest.json +9 -6
  95. package/templates/preview-runtime/v1/release-guard.mjs +1 -1
  96. package/templates/preview-runtime/v1/runtime.d.mts +1 -3
  97. package/templates/preview-runtime/v1/runtime.d.ts +1 -3
  98. package/templates/preview-runtime/v1/runtime.mjs +1142 -257
  99. package/templates/preview-runtime/v1/types/host.d.ts +29 -0
  100. package/templates/preview-runtime/v1/types/index.d.ts +6 -0
  101. package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
@@ -1,23 +1,26 @@
1
+ import { takesWorkspaceDirectory } from '@playcraft/common/cloud-workspace/runtime';
2
+ import { assertNoPendingLevelEditorTransaction } from '../workspaces/level-editor-atomic.js';
1
3
  import { withObservedFile } from '../workspaces/revisions.js';
2
- import { revisionScope, sessionWorkspaceRoot, workspaceRoot, workspaceSessions } from '../workspaces/registry.js';
4
+ import { revisionScope, sessionWorkspaceRoot, workspaceRoot, workspaceSessions, } from '../workspaces/registry.js';
3
5
  import { preflightSkillExecution } from '../workspaces/skill-execution-context.js';
4
- import { abandonTurnChanges, beginTurnChanges, finishTurnChanges } from '../workspaces/turn-changes.js';
6
+ import { abandonTurnChanges, beginTurnChanges, finishTurnChanges, } from '../workspaces/turn-changes.js';
5
7
  import { shareWorkspaceGit } from '../workspaces/shared-git.js';
6
- import { cancelPendingDirectoryAccess, holdDirectoryAccess, withOperationDirectoryAccess } from './directory-access.js';
8
+ import { cancelPendingDirectoryAccess, holdDirectoryAccess, withOperationDirectoryAccess, } from './directory-access.js';
7
9
  import { join } from 'node:path';
8
10
  import { z } from 'zod';
9
- import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, directoryRenamePayloadSchema, directoryDeletePayloadSchema, directoryMutationReceiptSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, workspaceModelPreviewBuildSchema, workspaceModelPreviewCancelSchema, } from '@playcraft/common/cloud-workspace/runtime';
11
+ import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, directoryRenamePayloadSchema, directoryDeletePayloadSchema, directoryMutationReceiptSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, hashOperationPayload, parameterPatchPayloadSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, levelEditorTaskRequestSchema, levelEditorVerifyPayloadSchema, levelEditorTransactionRequestSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, workspaceModelPreviewBuildSchema, workspaceModelPreviewCancelSchema, isSoundDocumentPath, } from '@playcraft/common/cloud-workspace/runtime';
10
12
  import { closeCliCredentials, ensureCliCredentials } from '../workspaces/cli-credentials.js';
11
13
  import { artifactFormat } from '../workspaces/artifact-files.js';
12
14
  import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
13
15
  import { resolveWorkspaceAttachments } from '../workspaces/attachments.js';
14
- import { importWorkspaceRemixReference, RemixReferenceError } from '../workspaces/remix-references.js';
16
+ import { importWorkspaceRemixReference, RemixReferenceError, } from '../workspaces/remix-references.js';
15
17
  import { ComposerContextError, resolveComposerContext } from '../workspaces/composer-context.js';
18
+ import { workspaceAudioAssetService } from './audio-assets.js';
16
19
  import { ensureParameterInitializationSkill } from '../workspaces/parameter-initialization-skill.js';
17
20
  import { readWorkspaceSkillSnapshot, readWorkspaceSkills } from '../workspaces/skills.js';
18
- import { loadWorkspaceSkillArtifact, workspacePlatformSkillAccess } from '../workspaces/skill-artifact-loader.js';
19
- import { enableLocalProjectSkill, syncLocalProjectSkills } from '../../project-skills/local-store.js';
20
- import { importBuiltinProjectSkills, migrateVideoProjectSkills } from '../../project-skills/builtin-import.js';
21
+ import { loadWorkspaceSkillArtifact, workspacePlatformSkillAccess, } from '../workspaces/skill-artifact-loader.js';
22
+ import { enableLocalProjectSkill, syncLocalProjectSkills, } from '../../project-skills/local-store.js';
23
+ import { importBuiltinProjectSkills, migrateVideoProjectSkills, } from '../../project-skills/builtin-import.js';
21
24
  import { NativePreparationError } from '../codex/native-adapter.js';
22
25
  import { workspaceUiSnapshotSchema } from '@playcraft/common/cloud-workspace/runtime';
23
26
  import { disposeTerminalHost } from './terminal-host.js';
@@ -31,19 +34,23 @@ import { ThemeAssetIndexError } from '../workspaces/theme-asset-index.js';
31
34
  import { patchWorkspaceParameters, readWorkspaceParameters } from '../workspaces/parameters.js';
32
35
  import { prepareParameterAgentTask, finishParameterAgentTask, verifyParameterAgentTask, } from '../workspaces/parameter-agent.js';
33
36
  import { previewFailureCode, previewWorkspaceRoot, runPreviewPublish, trackPreviewPublish, } from '../previews/publish.js';
34
- import { startManagedBackgroundWriter, stopBackgroundWriter, } from '../processes/managed-writes.js';
37
+ import { startManagedBackgroundWriter, stopBackgroundWriter } from '../processes/managed-writes.js';
35
38
  import { wakeSourceEventUploader } from './source-uploader.js';
36
39
  import { batchDirectoryHeld } from '../batch/occupancy.js';
37
40
  import { trustedMemberPolicy } from '../batch/policy.js';
38
- import { BinaryWorkspaceWriteError, observeWorkspaceBinaryFile, writeWorkspaceBinaryFile } from '../workspaces/binary-write.js';
41
+ import { BinaryWorkspaceWriteError, observeWorkspaceBinaryFile, writeWorkspaceBinaryFile, } from '../workspaces/binary-write.js';
39
42
  import { importWorkspaceLibraryAssets } from '../workspaces/asset-library-import.js';
40
- import { createWorkspaceDirectory, WorkspaceDirectoryCreateError } from '../workspaces/directory-create.js';
41
- import { DirectoryMutationError, mutateWorkspaceDirectory } from '../workspaces/directory-mutations.js';
43
+ import { createWorkspaceDirectory, WorkspaceDirectoryCreateError, } from '../workspaces/directory-create.js';
44
+ import { DirectoryMutationError, mutateWorkspaceDirectory, } from '../workspaces/directory-mutations.js';
42
45
  import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.js';
43
46
  import { publishAssetTransfer } from '../workspaces/asset-transfer-publish.js';
44
47
  import { AssetTransferError } from '../workspaces/asset-transfer-storage.js';
45
- import { runModelPreviewBuild, cancelModelPreview, modelPreviewError } from '../workspaces/procedural-3d/model-preview-build.js';
48
+ import { runModelPreviewBuild, cancelModelPreview, modelPreviewError, } from '../workspaces/procedural-3d/model-preview-build.js';
46
49
  import { runModelPreparation, trackModelPreparation } from '../workspaces/model-preparation.js';
50
+ import { prepareLevelEditorTask, finishLevelEditorTask, verifyLevelEditorTask, } from '../workspaces/level-editor-agent.js';
51
+ import { readWorkspaceLevelEditor, transactWorkspaceLevelEditor, reconcileWorkspaceLevelEditorTransaction, } from '../workspaces/level-editor.js';
52
+ import { readLevelEditorHistory } from '../workspaces/level-editor-history-store.js';
53
+ import { wakeLevelEditorRequests, ensureLevelEditorTaskPipeline, retryLevelEditorRequest, assertLevelEditorRetryInput, } from '../workspaces/level-editor-requests.js';
47
54
  const CONVERSATION_KINDS = new Set([
48
55
  'session.create',
49
56
  'session.turn',
@@ -75,6 +82,9 @@ const LOCAL_KINDS = new Set([
75
82
  'terminal.create',
76
83
  'parameter.patch',
77
84
  'parameter.verify',
85
+ 'level-editor.request',
86
+ 'level-editor.verify',
87
+ 'level-editor.transaction',
78
88
  'preview.publish',
79
89
  'model.preview',
80
90
  'model.preview.cancel',
@@ -94,7 +104,7 @@ export async function stopParameterVerifications(state) {
94
104
  }
95
105
  export async function recoverInterruptedParameterVerifications(journal) {
96
106
  for (const { scopeKey, operation } of journal.listOperations()) {
97
- if (operation.kind !== 'parameter.verify' || !['queued', 'dispatched', 'accepted', 'uncertain'].includes(operation.status))
107
+ if (!['parameter.verify', 'level-editor.verify'].includes(operation.kind) || !['queued', 'dispatched', 'accepted', 'uncertain'].includes(operation.status))
98
108
  continue;
99
109
  if (operation.status === 'queued' && journal.deferredDirectoryOperations().some(item => item.operationId === operation.operationId))
100
110
  continue;
@@ -116,9 +126,19 @@ export async function executeOperation(state, scopeKey, operation, payload, reco
116
126
  if (operation.kind === 'session.turn' && !recovery && operation.sessionId &&
117
127
  state.journal.getSession(operation.sessionId)?.recoverability === 'history_only')
118
128
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'native_not_recoverable' }));
119
- return withOperationDirectoryAccess(state, operation, () => {
129
+ return withOperationDirectoryAccess(state, operation, async () => {
120
130
  if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence || state.journal.destroyed())
121
131
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: state.journal.destroyed() ? 'runtime_destroyed' : 'stale_lease' }));
132
+ if (takesWorkspaceDirectory(operation.kind) && operation.kind !== 'level-editor.transaction' && operation.kind !== 'runtime.destroy') {
133
+ // `root` is the workspace container, not a registered directory: resolving it must go
134
+ // through the session root, or every root-scoped operation fails before it starts.
135
+ try {
136
+ await assertNoPendingLevelEditorTransaction(await sessionWorkspaceRoot(state, operation.workspaceId, operation.directoryName), { state, operation });
137
+ }
138
+ catch (error) {
139
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: levelEditorErrorCode(error) }));
140
+ }
141
+ }
122
142
  return executeScopedOperation(state, scopeKey, operation, payload, recovery);
123
143
  }, { operationId: operation.operationId, scopeKey, payload, ...(recovery ? { recovery } : {}) });
124
144
  };
@@ -195,6 +215,23 @@ async function executeScopedOperation(state, scopeKey, operation, payload, recov
195
215
  if (operation.kind === 'parameter.verify') {
196
216
  return executeParameterVerify(state, scopeKey, current, payload);
197
217
  }
218
+ if (operation.kind === 'level-editor.verify')
219
+ return executeLevelEditorVerify(state, scopeKey, current, payload);
220
+ if (operation.kind === 'level-editor.transaction')
221
+ return executeLevelEditorTransaction(state, scopeKey, current, payload);
222
+ if (operation.kind === 'level-editor.request') {
223
+ try {
224
+ const root = await workspaceRoot(state, current.workspaceId, current.directoryName);
225
+ const result = await retryLevelEditorRequest(state, current, payload, root);
226
+ const accepted = await persist(state, scopeKey, transitionOperation(current, 'accepted'));
227
+ const completed = await persist(state, scopeKey, transitionOperation(accepted, 'completed', { result }));
228
+ wakeLevelEditorRequests(state);
229
+ return completed;
230
+ }
231
+ catch (error) {
232
+ return persist(state, scopeKey, transitionOperation(current, 'failed', { error: levelEditorErrorCode(error) }));
233
+ }
234
+ }
198
235
  if (operation.kind === 'preview.publish') {
199
236
  return executePreviewPublish(state, scopeKey, current);
200
237
  }
@@ -399,11 +436,14 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
399
436
  const parameterTask = parameterAgentTaskRequestSchema.optional().safeParse(body.parameterTask);
400
437
  if (!parameterTask.success || (parameterTask.data && parameterTask.data.taskId !== operation.clientRequestId))
401
438
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_parameter_task' }));
402
- if (parameterTask.data) {
439
+ const levelEditorTask = levelEditorTaskRequestSchema.optional().safeParse(body.levelEditorTask);
440
+ if (!levelEditorTask.success || levelEditorTask.data && (parameterTask.data || levelEditorTask.data.taskId !== operation.clientRequestId))
441
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_level_editor_task' }));
442
+ if (parameterTask.data || levelEditorTask.data) {
403
443
  if (operation.directoryName === 'root')
404
444
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_workspace_directory' }));
405
445
  if ((selection.data.workMode ?? 'execute') !== 'execute' || requestedMode === 'read-only')
406
- return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_task_readonly' }));
446
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: levelEditorTask.data ? 'level_editor_task_requires_execute' : 'parameter_task_readonly' }));
407
447
  if (workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
408
448
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
409
449
  if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
@@ -439,7 +479,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
439
479
  if (policy?.directoryAccessMode === 'exclusive' && operation.directoryAccessMode !== 'exclusive')
440
480
  throw new ComposerContextError('requires-exclusive-turn');
441
481
  if (operation.skillExecutionPolicyVersion === 1 && (!policy ||
442
- (parameterTask.data ? 'exclusive' : policy.directoryAccessMode) !== operation.directoryAccessMode))
482
+ (parameterTask.data || levelEditorTask.data ? 'exclusive' : policy.directoryAccessMode) !== operation.directoryAccessMode))
443
483
  throw new ComposerContextError('context_skills_changed');
444
484
  }
445
485
  catch (error) {
@@ -452,8 +492,8 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
452
492
  let initializationSkill;
453
493
  try {
454
494
  let turnContext = selection.data.context;
455
- if (parameterTask.data?.action === 'initialize') {
456
- const selectedSkill = await ensureParameterInitializationSkill(root);
495
+ if (parameterTask.data?.action === 'initialize' || levelEditorTask.data) {
496
+ const selectedSkill = await ensureParameterInitializationSkill(root, levelEditorTask.data ? 'playcraft-level-editor' : 'playcraft-runtime-parameters', levelEditorTask.data ? { preserveExisting: true } : {});
457
497
  initializationSkill = { ...selectedSkill,
458
498
  path: join(root, '.playcraft/skills', selectedSkill.lockHash, selectedSkill.entryKey, 'SKILL.md') };
459
499
  turnContext = { ...turnContext, files: turnContext?.files ?? [], skills: [
@@ -465,13 +505,16 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
465
505
  directoryName: binding.directoryName ?? 'basic', context: turnContext,
466
506
  skillCache: join(state.config.stateDir, 'skill-artifacts'),
467
507
  loadSkillArtifact: entry => loadWorkspaceSkillArtifact(state, operation.workspaceId ?? 'workspace', entry),
508
+ ...(process.env.CLOUD_WORKSPACE_AUDIO_ASSETS_ENABLED === 'true' ? {
509
+ readAudioTarget: async (target) => (await workspaceAudioAssetService(state, operation.workspaceId, binding.directoryName ?? 'basic')).read(target),
510
+ } : {}),
468
511
  });
469
512
  }
470
513
  catch (error) {
471
514
  return persist(state, scopeKey, transitionOperation(operation, 'failed', {
472
- error: error instanceof ComposerContextError ? error.message : parameterTask.data?.action === 'initialize'
515
+ error: error instanceof ComposerContextError ? error.message : parameterTask.data?.action === 'initialize' || levelEditorTask.data
473
516
  ? error instanceof Error && /^(?:workspace_skills_|skills_revision_conflict$)/.test(error.message)
474
- ? error.message : parameterAgentError(error)
517
+ ? error.message : levelEditorTask.data ? 'level_editor_skill_unavailable' : parameterAgentError(error)
475
518
  : 'invalid_composer_context',
476
519
  }));
477
520
  }
@@ -489,14 +532,12 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
489
532
  })).join('\n') : '';
490
533
  const batchContext = operation.directoryName === 'root' && operation.workspaceId
491
534
  ? `PlayCraft batch request context (trusted runtime identity): ${JSON.stringify({ workspaceId: operation.workspaceId,
492
- clientRequestId: operation.clientRequestId, sourceDirectoryName: 'basic',
493
- ...(typeof body.teamId === 'string' ? { teamId: body.teamId } : {}) })}\n\n`
535
+ clientRequestId: operation.clientRequestId, sourceDirectoryName: 'basic' })}\n\n`
494
536
  : '';
495
537
  const engineeringBatchContext = state.config.workspaceMode === 'multi' && operation.workspaceId
496
538
  && operation.directoryName !== 'root' && !productionSessionAttemptId(sessionId) && !trustedMemberPolicy(state, sessionId)
497
539
  ? `PlayCraft batch request context (trusted runtime identity): ${JSON.stringify({ workspaceId: operation.workspaceId,
498
- clientRequestId: operation.clientRequestId, sourceDirectoryName: binding.directoryName ?? 'basic',
499
- ...(typeof body.teamId === 'string' ? { teamId: body.teamId } : {}) })}`
540
+ clientRequestId: operation.clientRequestId, sourceDirectoryName: binding.directoryName ?? 'basic' })}`
500
541
  : '';
501
542
  if (state.config.workspaceMode === 'multi') {
502
543
  try {
@@ -516,6 +557,14 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
516
557
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: parameterAgentError(error) }));
517
558
  }
518
559
  }
560
+ if (levelEditorTask.data) {
561
+ try {
562
+ parameterInstructions = (await prepareLevelEditorTask(state, operation, levelEditorTask.data, root, initializationSkill)).instructions;
563
+ }
564
+ catch (error) {
565
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: levelEditorErrorCode(error) }));
566
+ }
567
+ }
519
568
  // Taken before the native turn exists, so nothing the agent writes can precede it.
520
569
  await beginTurnChanges(state, sessionId, {
521
570
  root, workspaceId: operation.workspaceId, directoryName: binding.directoryName ?? 'basic',
@@ -548,7 +597,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
548
597
  permissionMode: selectedMode,
549
598
  workMode: selection.data.workMode ?? 'execute',
550
599
  context: composer.context,
551
- userText: selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data
600
+ userText: selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data || levelEditorTask.data
552
601
  ? String(body.userText ?? body.text ?? '') : undefined,
553
602
  updatedAt: new Date().toISOString(),
554
603
  });
@@ -569,7 +618,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
569
618
  }
570
619
  }
571
620
  catch (error) {
572
- if (!parameterTask.data)
621
+ if (!parameterTask.data && !levelEditorTask.data)
573
622
  throw error;
574
623
  abandonTurnChanges(state, sessionId);
575
624
  if (error instanceof NativePreparationError) {
@@ -631,7 +680,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
631
680
  threadId: binding.threadId,
632
681
  turnId: result.turnId,
633
682
  });
634
- if (selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data) {
683
+ if (selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data || levelEditorTask.data) {
635
684
  await recordNativeEvents(state, accepted, [{
636
685
  type: 'composer.context', occurredAt: new Date().toISOString(), payload: {
637
686
  threadId: binding.threadId, turnId: result.turnId, clientRequestId: operation.clientRequestId,
@@ -1168,6 +1217,8 @@ async function executeFileBinaryWrite(state, scopeKey, operation, payload) {
1168
1217
  if (!parsed.success)
1169
1218
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_binary_write' }));
1170
1219
  const input = parsed.data;
1220
+ if (isSoundDocumentPath(input.path))
1221
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unsupported_audio_protocol' }));
1171
1222
  const scope = revisionScope(state, operation.workspaceId, operation.directoryName);
1172
1223
  return state.journal.exclusive(`file:${scope ?? 'single'}:${input.path}`, async () => {
1173
1224
  try {
@@ -1211,6 +1262,8 @@ async function executeFileBinaryWrite(state, scopeKey, operation, payload) {
1211
1262
  }
1212
1263
  async function executeFileWrite(state, scopeKey, operation, payload) {
1213
1264
  const parsed = fileWritePayloadSchema.safeParse(payload);
1265
+ if (parsed.success && isSoundDocumentPath(parsed.data.path))
1266
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unsupported_audio_protocol' }));
1214
1267
  if (!parsed.success) {
1215
1268
  const message = parsed.error.issues[0]?.message;
1216
1269
  const error = message === 'write_id_required' || message === 'write_hold_conflict' || message === 'background_write_unmanaged'
@@ -1438,6 +1491,172 @@ async function executeGitIndex(state, scopeKey, operation, payload) {
1438
1491
  return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: gitErrorCode(error) }));
1439
1492
  }
1440
1493
  }
1494
+ async function withLevelEditorDocuments(state, operation, paths, action) {
1495
+ const observed = [...new Set(paths)].sort();
1496
+ const run = (index) => index === observed.length
1497
+ ? action()
1498
+ : withObservedFile(state, operation.workspaceId, observed[index], () => run(index + 1), operation.directoryName);
1499
+ return run(0);
1500
+ }
1501
+ async function executeLevelEditorTransaction(state, scopeKey, operation, payload) {
1502
+ const parsed = levelEditorTransactionRequestSchema.safeParse(payload);
1503
+ if (!parsed.success || parsed.data.requestId !== operation.clientRequestId)
1504
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_level_editor_transaction' }));
1505
+ const request = parsed.data;
1506
+ const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName), scope = revisionScope(state, operation.workspaceId, operation.directoryName);
1507
+ const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
1508
+ state,
1509
+ operation,
1510
+ });
1511
+ const ids = request.version === 1 ? [request.documentId] : Object.keys(request.expectedDocuments);
1512
+ const paths = ids.map((id) => snapshot.definition?.documents[id]?.path);
1513
+ if (paths.some((path) => !path) || !paths.length)
1514
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', {
1515
+ error: snapshot.diagnostics[0]?.code ?? 'level_editor_not_ready',
1516
+ }));
1517
+ return withLevelEditorDocuments(state, operation, paths, async () => {
1518
+ let applied = false;
1519
+ try {
1520
+ const written = await transactWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), request, { state, operation });
1521
+ applied = true;
1522
+ const files = written.results ?? [
1523
+ { ...written, documentId: request.version === 1 ? request.documentId : '' },
1524
+ ];
1525
+ for (const file of files)
1526
+ await state.journal.putFileRevision(file.path, { revision: file.revision, contentHash: file.contentHash }, scope);
1527
+ const revision = Math.max(...files.map((file) => file.revision));
1528
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { expectedRevision: revision }));
1529
+ await recordNativeEvents(state, accepted, files.map((file) => ({
1530
+ type: 'file.written',
1531
+ occurredAt: new Date().toISOString(),
1532
+ payload: { ...file, editorId: request.editorId },
1533
+ })));
1534
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed', {
1535
+ expectedRevision: revision,
1536
+ result: await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
1537
+ state,
1538
+ operation,
1539
+ }),
1540
+ }));
1541
+ }
1542
+ catch (error) {
1543
+ const code = applied ? 'level_editor_transaction_uncertain' : levelEditorErrorCode(error);
1544
+ return persist(state, scopeKey, transitionOperation(operation, code === 'level_editor_transaction_uncertain' ? 'uncertain' : 'failed', { error: code }));
1545
+ }
1546
+ });
1547
+ }
1548
+ /** Private receipts prove completion; a marker can only resume the previously authorized exact batch. */
1549
+ export async function reconcileLevelEditorTransaction(state, scopeKey, operation, payload) {
1550
+ if (operation.kind !== 'level-editor.transaction' ||
1551
+ !['dispatched', 'accepted', 'uncertain'].includes(operation.status) ||
1552
+ state.journal.destroyed())
1553
+ return undefined;
1554
+ const result = await state.journal.exclusive(`workspace:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`, () => withOperationDirectoryAccess(state, operation, async () => {
1555
+ const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
1556
+ const trusted = payload === undefined
1557
+ ? await readLevelEditorHistory(root, { state, operation }, operation.clientRequestId)
1558
+ : undefined;
1559
+ const parsed = levelEditorTransactionRequestSchema.safeParse(payload ?? trusted?.request);
1560
+ if (!parsed.success ||
1561
+ parsed.data.requestId !== operation.clientRequestId ||
1562
+ hashOperationPayload(parsed.data) !== operation.payloadHash)
1563
+ return operation;
1564
+ const request = parsed.data, scope = revisionScope(state, operation.workspaceId, operation.directoryName);
1565
+ const initial = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
1566
+ state,
1567
+ operation,
1568
+ });
1569
+ const ids = request.version === 1 ? [request.documentId] : Object.keys(request.expectedDocuments);
1570
+ const paths = ids.map((id) => initial.definition?.documents[id]?.path);
1571
+ if (paths.some((path) => !path))
1572
+ return operation;
1573
+ return withLevelEditorDocuments(state, operation, paths, async () => {
1574
+ const proof = await reconcileWorkspaceLevelEditorTransaction(root, state.journal.fileRevisions(scope), request, { state, operation });
1575
+ if (!proof)
1576
+ return operation;
1577
+ const files = proof.results ?? [
1578
+ {
1579
+ path: proof.path,
1580
+ contentHash: proof.contentHash,
1581
+ size: proof.size,
1582
+ revision: proof.revision,
1583
+ documentId: request.version === 1 ? request.documentId : '',
1584
+ },
1585
+ ];
1586
+ for (const file of files) {
1587
+ await state.journal.putFileRevision(file.path, { revision: file.revision, contentHash: file.contentHash }, scope);
1588
+ const duplicate = state.journal.listEvents().some((event) => {
1589
+ const body = asRecord(event.payload);
1590
+ return (body.operationId === operation.operationId &&
1591
+ event.type === 'file.written' &&
1592
+ body.contentHash === file.contentHash &&
1593
+ body.revision === file.revision &&
1594
+ body.documentId === file.documentId);
1595
+ });
1596
+ if (!duplicate)
1597
+ await recordNativeEvents(state, operation, [
1598
+ {
1599
+ type: 'file.written',
1600
+ occurredAt: new Date().toISOString(),
1601
+ payload: { ...file, editorId: request.editorId },
1602
+ },
1603
+ ]);
1604
+ }
1605
+ const current = operation.status === 'dispatched'
1606
+ ? transitionOperation(operation, 'accepted')
1607
+ : operation;
1608
+ return persist(state, scopeKey, transitionOperation(current, 'completed', {
1609
+ expectedRevision: Math.max(...files.map((file) => file.revision)),
1610
+ result: proof.snapshot,
1611
+ error: undefined,
1612
+ }));
1613
+ });
1614
+ }));
1615
+ return result.status === 'completed' ? result : undefined;
1616
+ }
1617
+ async function executeLevelEditorVerify(state, scopeKey, operation, payload) {
1618
+ const parsed = levelEditorVerifyPayloadSchema.safeParse(payload);
1619
+ if (!parsed.success)
1620
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_level_editor_task' }));
1621
+ if (operation.directoryName === 'root' || workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
1622
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
1623
+ if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
1624
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
1625
+ const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
1626
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
1627
+ const controller = new AbortController();
1628
+ const assertActive = () => {
1629
+ if (controller.signal.aborted ||
1630
+ parameterVerificationClosing.has(state) ||
1631
+ state.journal.destroyed())
1632
+ throw new Error('level_editor_verification_interrupted');
1633
+ if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence)
1634
+ throw new Error('stale_lease');
1635
+ };
1636
+ const key = `workspace:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`;
1637
+ const task = state.journal.exclusive(key, async () => {
1638
+ try {
1639
+ await assertLevelEditorRetryInput(state, operation, parsed.data, root);
1640
+ const result = await verifyLevelEditorTask(state, operation, parsed.data, root, {
1641
+ assertActive,
1642
+ signal: controller.signal,
1643
+ });
1644
+ assertActive();
1645
+ await persist(state, scopeKey, transitionOperation(accepted, 'completed', { result }));
1646
+ }
1647
+ catch (error) {
1648
+ await persist(state, scopeKey, transitionOperation(accepted, 'failed', { error: levelEditorErrorCode(error) }));
1649
+ }
1650
+ });
1651
+ const tasks = parameterVerifications.get(state) ?? new Map();
1652
+ parameterVerifications.set(state, tasks);
1653
+ const tracked = task.catch(() => undefined).finally(() => tasks.delete(controller));
1654
+ tasks.set(controller, tracked);
1655
+ return accepted;
1656
+ }
1657
+ function levelEditorErrorCode(error) {
1658
+ return error instanceof Error && /^(?:level_editor_|invalid_level_editor_|revision_conflict|stale_lease|runtime_destroyed)/.test(error.message) ? error.message : 'level_editor_operation_failed';
1659
+ }
1441
1660
  async function executeParameterPatch(state, scopeKey, operation, payload) {
1442
1661
  const parsed = parameterPatchPayloadSchema.safeParse(payload);
1443
1662
  if (!parsed.success) {
@@ -1729,6 +1948,10 @@ async function syncParameterAgentOperation(state, operation) {
1729
1948
  return;
1730
1949
  if (operation.status === 'completed' || operation.status === 'failed' || operation.status === 'interrupted' || operation.status === 'uncertain') {
1731
1950
  await finishParameterAgentTask(state, operation, operation.status, operation.error);
1951
+ await finishLevelEditorTask(state, operation, operation.status, operation.error);
1952
+ if (operation.status === 'completed' && /^[a-f0-9-]{36}$/i.test(operation.clientRequestId))
1953
+ await ensureLevelEditorTaskPipeline(state, operation);
1954
+ wakeLevelEditorRequests(state);
1732
1955
  }
1733
1956
  }
1734
1957
  function native(state) {
@@ -1,3 +1,4 @@
1
+ import { hasPendingLevelEditorTransaction, assertNoPendingLevelEditorTransaction } from '../workspaces/level-editor-atomic.js';
1
2
  import { readObservedFile, withObservedFile } from '../workspaces/revisions.js';
2
3
  import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.js';
3
4
  import { registerWorkspace, revisionScope, sessionWorkspaceRoot, workspaceRoot, WorkspaceContextError } from '../workspaces/registry.js';
@@ -11,6 +12,9 @@ import { InvalidWorkspacePathError, } from '../workspaces/files.js';
11
12
  import { GitWorkspaceError, readWorkspaceGitDiff, readWorkspaceGitFile, readWorkspaceGitGraph, readWorkspaceGitStatus, } from '../workspaces/git.js';
12
13
  import { readWorkspaceParameters } from '../workspaces/parameters.js';
13
14
  import { listParameterAgentTasks } from '../workspaces/parameter-agent.js';
15
+ import { readWorkspaceLevelEditor } from '../workspaces/level-editor.js';
16
+ import { listLevelEditorTasks } from '../workspaces/level-editor-agent.js';
17
+ import { listLevelEditorRequests } from '../workspaces/level-editor-requests.js';
14
18
  import { readDependencies } from '../workspaces/dependencies.js';
15
19
  import { permissionDriftFromError } from '../workspaces/workspace-shares.js';
16
20
  import { desktopHost, desktopRoute } from './desktop-host.js';
@@ -36,7 +40,7 @@ import { probeRuntimeConnectivity } from './connectivity.js';
36
40
  import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
37
41
  import { WorkspaceArtifactStore } from '../workspaces/artifacts.js';
38
42
  import { OperationLogStore } from '../workspaces/operation-log.js';
39
- import { acceptOperationCore } from './accept-operation.js';
43
+ import { acceptOperationCore, observeLevelEditorTransaction } from './accept-operation.js';
40
44
  import { workspaceArtifactContentQuerySchema } from '@playcraft/common/cloud-workspace/runtime';
41
45
  import { searchWorkspaceFiles } from '../workspaces/files-search.js';
42
46
  import { serveWorkspaceArchive } from '../workspaces/archive.js';
@@ -50,11 +54,43 @@ import { withDirectoryWrite } from './directory-access.js';
50
54
  import { readProceduralModelCandidates } from '../workspaces/procedural-3d/read-candidates.js';
51
55
  import { ModelPreviewStore } from '../workspaces/procedural-3d/model-preview-store.js';
52
56
  import { readModelPreparation } from '../workspaces/model-preparation.js';
57
+ import { serveAudioExport } from './audio-export.js';
58
+ import { handleAudioAssetRequest } from './audio-assets.js';
53
59
  export async function handleRuntimeRequest(state, request, response) {
54
60
  const url = new URL(request.url ?? '/', 'http://127.0.0.1');
55
61
  const workspaceId = url.searchParams.get('workspaceId');
62
+ const protectedRead = ['/runtime/level-editor', '/runtime/parameters', '/runtime/file', '/runtime/files', '/runtime/files/search', '/runtime/file-content', '/runtime/archive', '/runtime/git-bundle', '/runtime/git/status', '/runtime/git/diff', '/runtime/audio/assets', '/runtime/audio/content'].includes(url.pathname) && ['GET', 'HEAD'].includes(request.method ?? 'GET');
63
+ if (authorizeRuntime(state, request) === 'control' && protectedRead) {
64
+ try {
65
+ const directoryName = url.searchParams.get('directoryName') ?? 'basic';
66
+ const readContext = { state, workspaceId: workspaceId ?? undefined, directoryName };
67
+ const root = await workspaceRoot(state, readContext.workspaceId, directoryName);
68
+ if (await hasPendingLevelEditorTransaction(root, readContext)) {
69
+ // A retained writer owns the exclusive lease. Reconcile that exact operation before admitting a reader.
70
+ for (const { operation } of state.journal.listOperations())
71
+ if (operation.kind === 'level-editor.transaction' && (state.config.workspaceMode !== 'multi' || operation.workspaceId === readContext.workspaceId) && (operation.directoryName ?? 'basic') === directoryName)
72
+ await observeLevelEditorTransaction(state, operation);
73
+ }
74
+ const recovering = await hasPendingLevelEditorTransaction(root, readContext);
75
+ return await withDirectoryWrite(state, { ...readContext, mode: recovering ? 'exclusive' : 'shared', wait: false }, async () => {
76
+ if (recovering) {
77
+ const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(revisionScope(state, readContext.workspaceId, directoryName)), readContext);
78
+ if (snapshot.status === 'invalid' && snapshot.diagnostics.some(item => /recovery|history/.test(item.code)))
79
+ throw new Error(snapshot.diagnostics[0].code);
80
+ }
81
+ await assertNoPendingLevelEditorTransaction(root, readContext);
82
+ return dispatchRuntimeRequest(state, request, response);
83
+ });
84
+ }
85
+ catch (error) {
86
+ if (!response.headersSent)
87
+ writeJson(response, 409, { error: error instanceof Error ? error.message : 'level_editor_recovery_required' });
88
+ return;
89
+ }
90
+ }
56
91
  // Body-scoped operations, members and terminal control take their own lifetime-aware leases.
57
92
  const ownLease = url.pathname.startsWith('/runtime/operations') || url.pathname.startsWith('/runtime/terminals/') ||
93
+ url.pathname === '/runtime/audio/export' ||
58
94
  url.pathname.startsWith('/runtime/batch-') || /^\/runtime\/sessions\/[^/]+\/ui-results$/.test(url.pathname) ||
59
95
  /^\/runtime\/sessions\/[^/]+\/prediction$/.test(url.pathname);
60
96
  if (authorizeRuntime(state, request) === 'control' && workspaceId &&
@@ -87,6 +123,15 @@ async function dispatchRuntimeRequest(state, request, response) {
87
123
  return;
88
124
  }
89
125
  try {
126
+ if (request.method === 'POST' && url.pathname === '/runtime/audio/export') {
127
+ if (writeDestroyed(state, response))
128
+ return;
129
+ await sessionWorkspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName);
130
+ await serveAudioExport(request, response, url);
131
+ return;
132
+ }
133
+ if (await handleAudioAssetRequest(state, request, response, url))
134
+ return;
90
135
  if (await handleWorkspaceStorage(state, request, response, url))
91
136
  return;
92
137
  const transfer = await handleAssetTransferRequest(state, request, url);
@@ -135,6 +180,10 @@ async function dispatchRuntimeRequest(state, request, response) {
135
180
  instanceToken: state.instanceToken,
136
181
  permissionsVersion: 1,
137
182
  composerContextVersion: 1,
183
+ audioAssetVersion: 1,
184
+ audioContextVersion: 1,
185
+ audioScoreVersion: 1,
186
+ audioScoreContextVersion: 1,
138
187
  scheduledTaskIdentityVersion: 1,
139
188
  scheduledTaskCancellationVersion: 1,
140
189
  ...(state.native?.scheduledTaskProposalSource ? { scheduledTaskProposalVersion: 1 } : {}),
@@ -142,6 +191,12 @@ async function dispatchRuntimeRequest(state, request, response) {
142
191
  directoryConcurrencyVersion: 1,
143
192
  skillExecutionPolicyVersion: 1,
144
193
  parameterInitializationVersion: 1,
194
+ levelEditorVersion: 1,
195
+ levelEditorTaskVersion: 1,
196
+ levelEditorHistoryVersion: 1,
197
+ levelEditorTransactionVersion: 2,
198
+ levelEditorExtensionVersion: 1,
199
+ levelEditorRetryVersion: 1,
145
200
  remixReferencesVersion: 1,
146
201
  batchSourceFreezeRevision: 2,
147
202
  ...(state.native?.workspaceUi ? { workspaceUiVersion: 1 } : {}),
@@ -789,6 +844,44 @@ async function dispatchRuntimeRequest(state, request, response) {
789
844
  await writeGitRead(state, response, async () => readWorkspaceGitGraph(await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName)));
790
845
  return;
791
846
  }
847
+ if (request.method === 'GET' && url.pathname === '/runtime/level-editor') {
848
+ if (writeDestroyed(state, response))
849
+ return;
850
+ if (directoryName === 'root') {
851
+ writeJson(response, 400, { error: 'invalid_workspace_directory' });
852
+ return;
853
+ }
854
+ const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
855
+ const root = await workspaceRoot(state, workspaceId, directoryName), scope = revisionScope(state, workspaceId, directoryName);
856
+ const first = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), { state, workspaceId, directoryName });
857
+ for (const document of Object.values(first.definition?.documents ?? {}))
858
+ await withObservedFile(state, workspaceId, document.path, async () => undefined, directoryName);
859
+ writeJson(response, 200, await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), { state, workspaceId, directoryName }));
860
+ return;
861
+ }
862
+ if (request.method === 'GET' && url.pathname === '/runtime/level-editor/tasks') {
863
+ if (writeDestroyed(state, response))
864
+ return;
865
+ if (directoryName === 'root') {
866
+ writeJson(response, 400, { error: 'invalid_workspace_directory' });
867
+ return;
868
+ }
869
+ const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
870
+ writeJson(response, 200, { tasks: await listLevelEditorTasks(state, workspaceId, directoryName, await workspaceRoot(state, workspaceId, directoryName)) });
871
+ return;
872
+ }
873
+ if (request.method === 'GET' && url.pathname === '/runtime/level-editor/requests') {
874
+ if (writeDestroyed(state, response))
875
+ return;
876
+ if (directoryName === 'root') {
877
+ writeJson(response, 400, { error: 'invalid_workspace_directory' });
878
+ return;
879
+ }
880
+ const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
881
+ await workspaceRoot(state, workspaceId, directoryName);
882
+ writeJson(response, 200, { requests: await listLevelEditorRequests(state, workspaceId, directoryName) });
883
+ return;
884
+ }
792
885
  if (request.method === 'GET' && url.pathname === '/runtime/parameters') {
793
886
  if (writeDestroyed(state, response))
794
887
  return;
@@ -839,7 +932,8 @@ async function dispatchRuntimeRequest(state, request, response) {
839
932
  directoryName,
840
933
  sessionId: url.searchParams.get('sessionId') ?? undefined,
841
934
  }));
842
- writeJson(response, operation ? 200 : 404, operation ?? { error: 'operation_not_found' });
935
+ const observed = operation ? await observeLevelEditorTransaction(state, operation) : undefined;
936
+ writeJson(response, observed ? 200 : 404, observed ?? { error: 'operation_not_found' });
843
937
  return;
844
938
  }
845
939
  const operationLog = /^\/runtime\/operations\/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\/log$/.exec(url.pathname);
@@ -873,7 +967,7 @@ async function dispatchRuntimeRequest(state, request, response) {
873
967
  writeJson(response, 404, { error: 'operation_not_found' });
874
968
  return;
875
969
  }
876
- writeJson(response, 200, operation);
970
+ writeJson(response, 200, await observeLevelEditorTransaction(state, operation));
877
971
  return;
878
972
  }
879
973
  if (request.method === 'POST' && url.pathname === '/runtime/scheduled-task-proposals/source') {
@@ -11,7 +11,7 @@ export function buildOperationRecord(state, parsed, payloadHash, leaseFence, exe
11
11
  ...(parsed.gatewayOperationId ? { gatewayOperationId: parsed.gatewayOperationId } : {}),
12
12
  ...(policy ? { skillExecutionPolicyVersion: policy.skillExecutionPolicyVersion, directoryAccessMode: policy.directoryAccessMode,
13
13
  ...workspaceComposerSelectionSchema.parse({ context: payload.context, workMode: payload.workMode }) } : {}),
14
- ...(parsed.kind === 'session.turn' && payload.parameterTask !== undefined ? { directoryAccessMode: 'exclusive' } : {}),
14
+ ...(parsed.kind === 'session.turn' && (payload.parameterTask !== undefined || payload.levelEditorTask !== undefined) ? { directoryAccessMode: 'exclusive' } : {}),
15
15
  environment: state.config.environment,
16
16
  userId: state.config.userId,
17
17
  clientRequestId: parsed.clientRequestId,