@playcraft/cli 0.0.57 → 0.0.58

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 (225) hide show
  1. package/README.md +34 -16
  2. package/dist/cli-root-help.js +1 -0
  3. package/dist/commands/build-all.js +10 -9
  4. package/dist/commands/build.js +13 -12
  5. package/dist/commands/create.js +26 -33
  6. package/dist/commands/platform-skills.generated.js +19 -0
  7. package/dist/commands/skills.js +2 -0
  8. package/dist/commands/tools-generation.js +1 -1
  9. package/dist/commands/workspace-runtime.js +54 -0
  10. package/dist/index.js +3 -1
  11. package/dist/project-skills/commands.js +72 -0
  12. package/dist/project-skills/lifecycle.js +28 -0
  13. package/dist/project-skills/local-run.js +112 -0
  14. package/dist/project-skills/local-store.js +149 -0
  15. package/dist/project-skills/messages.js +72 -0
  16. package/dist/project-skills/reconcile.js +62 -0
  17. package/dist/project-skills/remote-cache.js +51 -0
  18. package/dist/project-skills/validation.js +19 -0
  19. package/dist/remix/clone.js +2 -0
  20. package/dist/remix/init-template.js +2 -0
  21. package/dist/remix/pull.js +2 -0
  22. package/dist/remix/push.js +21 -1
  23. package/dist/utils/agent-api-client.js +54 -17
  24. package/dist/utils/tool-operation-journal.js +54 -0
  25. package/dist/workspace-runtime/codex/app-server.js +395 -0
  26. package/dist/workspace-runtime/codex/config-toml.js +25 -0
  27. package/dist/workspace-runtime/codex/jsonrpc-stdio.js +106 -0
  28. package/dist/workspace-runtime/codex/loopback.js +59 -0
  29. package/dist/workspace-runtime/codex/native-adapter.js +3 -0
  30. package/dist/workspace-runtime/main.js +10 -0
  31. package/dist/workspace-runtime/persistence/journal.js +350 -0
  32. package/dist/workspace-runtime/processes/managed-writes.js +69 -0
  33. package/dist/workspace-runtime/serve.js +111 -0
  34. package/dist/workspace-runtime/server/auth.js +21 -0
  35. package/dist/workspace-runtime/server/dispatch.js +861 -0
  36. package/dist/workspace-runtime/server/execution-group.js +88 -0
  37. package/dist/workspace-runtime/server/http.js +561 -0
  38. package/dist/workspace-runtime/server/lock.js +53 -0
  39. package/dist/workspace-runtime/server/types.js +1 -0
  40. package/dist/workspace-runtime/workspaces/context-error.js +2 -0
  41. package/dist/workspace-runtime/workspaces/file-snapshot.js +213 -0
  42. package/dist/workspace-runtime/workspaces/files.js +143 -0
  43. package/dist/workspace-runtime/workspaces/git.js +242 -0
  44. package/dist/workspace-runtime/workspaces/json5-edit.js +170 -0
  45. package/dist/workspace-runtime/workspaces/parameters.js +272 -0
  46. package/dist/workspace-runtime/workspaces/prepare.js +364 -0
  47. package/dist/workspace-runtime/workspaces/registry.js +107 -0
  48. package/dist/workspace-runtime/workspaces/revisions.js +31 -0
  49. package/package.json +4 -1
  50. package/project-template-v2/.claude/agents/artist.md +82 -0
  51. package/project-template-v2/.claude/agents/developer.md +153 -0
  52. package/project-template-v2/.claude/agents/game-designer.md +264 -0
  53. package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +533 -0
  54. package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +153 -0
  55. package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +27 -0
  56. package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +208 -0
  57. package/project-template-v2/.claude/agents/refs/atom-skill-library.md +81 -0
  58. package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +432 -0
  59. package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +252 -0
  60. package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +266 -0
  61. package/project-template-v2/.claude/agents/refs/framework-mda.md +147 -0
  62. package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +123 -0
  63. package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +88 -0
  64. package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  65. package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  66. package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +300 -0
  67. package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  68. package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +1141 -0
  69. package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +111 -0
  70. package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  71. package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +412 -0
  72. package/project-template-v2/.claude/agents/reviewer.md +75 -0
  73. package/project-template-v2/.claude/agents/technical-artist.md +86 -0
  74. package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +243 -0
  75. package/project-template-v2/.claude/hooks/user-prompt.mjs +133 -0
  76. package/project-template-v2/.claude/settings.json +33 -0
  77. package/project-template-v2/.claude/skills/brainstorming/SKILL.md +161 -0
  78. package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +270 -0
  79. package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +177 -0
  80. package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +354 -0
  81. package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +148 -0
  82. package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +56 -0
  83. package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +62 -0
  84. package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  85. package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +309 -0
  86. package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  87. package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  88. package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +205 -0
  89. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  90. package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  91. package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  92. package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +298 -0
  93. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  94. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  95. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +324 -0
  96. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  97. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  98. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  99. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  100. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  101. package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  102. package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +219 -0
  103. package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +373 -0
  104. package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +161 -0
  105. package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +215 -0
  106. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +382 -0
  107. package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  108. package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +158 -0
  109. package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +100 -0
  110. package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  111. package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  112. package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  113. package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  114. package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +390 -0
  115. package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  116. package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  117. package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  118. package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +240 -0
  119. package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +226 -0
  120. package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +308 -0
  121. package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  122. package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +134 -0
  123. package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +142 -0
  124. package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +153 -0
  125. package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +386 -0
  126. package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +182 -0
  127. package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +72 -0
  128. package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +121 -0
  129. package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +102 -0
  130. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +127 -0
  131. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +106 -0
  132. package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +128 -0
  133. package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +67 -0
  134. package/project-template-v2/.claude/snapshot.mjs +263 -0
  135. package/project-template-v2/.playcraft/skills.lock.json +152 -0
  136. package/project-template-v2/CLAUDE.md +146 -0
  137. package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
  138. package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
  139. package/project-template-v2/assets/bundles/.gitkeep +0 -0
  140. package/project-template-v2/assets/images/bg/.gitkeep +0 -0
  141. package/project-template-v2/assets/images/reference/.gitkeep +0 -0
  142. package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
  143. package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
  144. package/project-template-v2/assets/images/ui/.gitkeep +0 -0
  145. package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
  146. package/project-template-v2/assets/models/.gitkeep +0 -0
  147. package/project-template-v2/docs/harness/iteration-1/context-flow.md +254 -0
  148. package/project-template-v2/docs/harness/iteration-1/generate-flow.md +91 -0
  149. package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +214 -0
  150. package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +75 -0
  151. package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +63 -0
  152. package/project-template-v2/docs/harness/iteration-2/context-flow.md +223 -0
  153. package/project-template-v2/docs/harness/iteration-2/generate-flow.md +129 -0
  154. package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +267 -0
  155. package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +164 -0
  156. package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +115 -0
  157. package/project-template-v2/docs/harness/orchestrator-flow.md +364 -0
  158. package/project-template-v2/docs/project-state.json +60 -0
  159. package/project-template-v2/docs/project-state.md +72 -0
  160. package/project-template-v2/docs/standards/README.md +225 -0
  161. package/project-template-v2/docs/standards/agent-behavior-standards.md +174 -0
  162. package/project-template-v2/docs/standards/artifacts/design-brief.md +19 -0
  163. package/project-template-v2/docs/standards/artifacts/design.md +22 -0
  164. package/project-template-v2/docs/standards/artifacts/game-code.md +41 -0
  165. package/project-template-v2/docs/standards/artifacts/todo-list.md +41 -0
  166. package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +343 -0
  167. package/project-template-v2/game/index.ts +18 -0
  168. package/project-template-v2/globals.d.ts +51 -0
  169. package/project-template-v2/index.css +34 -0
  170. package/project-template-v2/index.html +18 -0
  171. package/project-template-v2/main.ts +9 -0
  172. package/project-template-v2/package.json +46 -0
  173. package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +31 -0
  174. package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +86 -0
  175. package/project-template-v2/skills/_shared/scripts/dod-check.mjs +153 -0
  176. package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +70 -0
  177. package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +91 -0
  178. package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +131 -0
  179. package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +80 -0
  180. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +90 -0
  181. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +93 -0
  182. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +23 -0
  183. package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +230 -0
  184. package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +58 -0
  185. package/project-template-v2/skills/_shared/scripts/state-get.mjs +339 -0
  186. package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +39 -0
  187. package/project-template-v2/skills/_shared/scripts/state-set.mjs +94 -0
  188. package/project-template-v2/skills/_shared/scripts/state-store.mjs +783 -0
  189. package/project-template-v2/skills/_shared/scripts/todo-add.mjs +88 -0
  190. package/project-template-v2/skills/_shared/scripts/todo-get.mjs +130 -0
  191. package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +33 -0
  192. package/project-template-v2/skills/_shared/scripts/todo-set.mjs +47 -0
  193. package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +268 -0
  194. package/project-template-v2/skills/_shared/scripts/verify-env.mjs +285 -0
  195. package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +161 -0
  196. package/project-template-v2/skills/playable-autoplay/SKILL.md +176 -0
  197. package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +4 -0
  198. package/project-template-v2/skills/playable-debug/SKILL.md +116 -0
  199. package/project-template-v2/skills/playable-debug/agents/openai.yaml +4 -0
  200. package/project-template-v2/skills/playable-debug/references/debug-config.md +40 -0
  201. package/project-template-v2/skills/playable-record/SKILL.md +140 -0
  202. package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +104 -0
  203. package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +141 -0
  204. package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +425 -0
  205. package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +261 -0
  206. package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +398 -0
  207. package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +322 -0
  208. package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +105 -0
  209. package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +230 -0
  210. package/project-template-v2/skills/playable-validate/SKILL.md +61 -0
  211. package/project-template-v2/skills/playable-validate/validation-rules.md +14 -0
  212. package/project-template-v2/skills/playable-verify-ui/SKILL.md +63 -0
  213. package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +1 -0
  214. package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +218 -0
  215. package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +51 -0
  216. package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +59 -0
  217. package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +106 -0
  218. package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +116 -0
  219. package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +59 -0
  220. package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +64 -0
  221. package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +58 -0
  222. package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +333 -0
  223. package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
  224. package/project-template-v2/tsconfig.json +20 -0
  225. package/project-template-v2/vite.config.ts +27 -0
@@ -0,0 +1,861 @@
1
+ import { withObservedFile } from '../workspaces/revisions.js';
2
+ import { revisionScope, workspaceRoot, workspaceSessions } from '../workspaces/registry.js';
3
+ import { join } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { checkpointRecordSchema, fileWritePayloadSchema, gitCommitPayloadSchema, gitPushPayloadSchema, hashOperationPayload, nextSourceSequence, parameterPatchPayloadSchema, pendingSourceEventSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, } from '@playcraft/common/cloud-workspace/runtime';
6
+ import { captureWorkspaceTree, materializeSnapshot, publishFileSnapshot, } from '../workspaces/file-snapshot.js';
7
+ import { InvalidWorkspacePathError, resolveWorkspaceFile, restoreWorkspaceFiles, snapshotWorkspaceFiles, writeWorkspaceFile, } from '../workspaces/files.js';
8
+ import { GitWorkspaceError, commitWorkspaceGit, pushWorkspaceGit } from '../workspaces/git.js';
9
+ import { ParameterEditError } from '../workspaces/json5-edit.js';
10
+ import { patchWorkspaceParameters, readWorkspaceParameters } from '../workspaces/parameters.js';
11
+ import { executionGroupRestarted } from './execution-group.js';
12
+ import { probeProcess, spawnBackgroundWriter, stopBackgroundWriter, } from '../processes/managed-writes.js';
13
+ const CONVERSATION_KINDS = new Set([
14
+ 'session.create',
15
+ 'session.turn',
16
+ 'session.interrupt',
17
+ 'session.approval.respond',
18
+ ]);
19
+ const LOCAL_KINDS = new Set([
20
+ 'runtime.ensure',
21
+ 'runtime.destroy',
22
+ 'checkpoint.create',
23
+ 'checkpoint.restore',
24
+ 'file.write',
25
+ 'git.commit',
26
+ 'git.push',
27
+ 'terminal.create',
28
+ 'parameter.patch',
29
+ ]);
30
+ export function canExecuteKind(state, kind) {
31
+ if (LOCAL_KINDS.has(kind))
32
+ return true;
33
+ if (kind === 'session.turn' && !workspaceTurnAllowed(state))
34
+ return true;
35
+ if (state.journal.destroyed() && CONVERSATION_KINDS.has(kind))
36
+ return false;
37
+ return Boolean(state.native?.ready()) && CONVERSATION_KINDS.has(kind);
38
+ }
39
+ export async function executeOperation(state, scopeKey, operation, payload) {
40
+ const run = async () => {
41
+ if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence || state.journal.destroyed()) {
42
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: state.journal.destroyed() ? 'runtime_destroyed' : 'stale_lease' }));
43
+ }
44
+ return executeScopedOperation(state, scopeKey, operation, payload);
45
+ };
46
+ const checkpointId = asRecord(payload).checkpointId;
47
+ const checkpointRun = () => operation.kind.startsWith('checkpoint.') && typeof checkpointId === 'string'
48
+ ? state.journal.exclusive(`checkpoint:${checkpointId}`, run) : run();
49
+ if (['runtime.destroy', 'session.interrupt', 'session.approval.respond'].includes(operation.kind))
50
+ return run();
51
+ return state.journal.exclusive(`workspace:${revisionScope(state, operation.workspaceId) ?? 'single'}`, checkpointRun);
52
+ }
53
+ function workspaceTurnAllowed(state) {
54
+ const gate = state.rebuildGate;
55
+ if (!gate)
56
+ return true;
57
+ return gate.directoryReady && gate.repositoryReady && gate.environmentReady;
58
+ }
59
+ async function executeScopedOperation(state, scopeKey, operation, payload) {
60
+ let current = await persist(state, scopeKey, transitionOperation(operation, 'dispatched'));
61
+ if (operation.kind === 'runtime.ensure') {
62
+ current = await persist(state, scopeKey, transitionOperation(current, 'accepted'));
63
+ return persist(state, scopeKey, transitionOperation(current, 'completed'));
64
+ }
65
+ if (operation.kind === 'runtime.destroy') {
66
+ return executeDestroy(state, scopeKey, current);
67
+ }
68
+ if (state.journal.destroyed()) {
69
+ return persist(state, scopeKey, transitionOperation(current, 'failed', { error: 'runtime_destroyed' }));
70
+ }
71
+ if (operation.kind === 'session.turn' && !workspaceTurnAllowed(state)) {
72
+ return persist(state, scopeKey, transitionOperation(current, 'failed', { error: 'workspace_not_ready' }));
73
+ }
74
+ if (operation.kind === 'checkpoint.create') {
75
+ return executeCheckpoint(state, scopeKey, current, payload);
76
+ }
77
+ if (operation.kind === 'checkpoint.restore') {
78
+ return executeRestore(state, scopeKey, current, payload);
79
+ }
80
+ if (operation.kind === 'file.write') {
81
+ return executeFileWrite(state, scopeKey, current, payload);
82
+ }
83
+ if (operation.kind === 'git.commit') {
84
+ return executeGitCommit(state, scopeKey, current, payload);
85
+ }
86
+ if (operation.kind === 'git.push') {
87
+ return executeGitPush(state, scopeKey, current, payload);
88
+ }
89
+ if (operation.kind === 'terminal.create') {
90
+ return executeTerminalCreate(state, scopeKey, current, payload);
91
+ }
92
+ if (operation.kind === 'parameter.patch') {
93
+ return executeParameterPatch(state, scopeKey, current, payload);
94
+ }
95
+ if (!state.native?.ready()) {
96
+ return persist(state, scopeKey, transitionOperation(current, 'failed', { error: 'capability_unavailable' }));
97
+ }
98
+ try {
99
+ if (operation.kind === 'session.create') {
100
+ return executeCreate(state, scopeKey, current);
101
+ }
102
+ if (operation.kind === 'session.turn') {
103
+ return executeTurn(state, scopeKey, current, payload);
104
+ }
105
+ if (operation.kind === 'session.interrupt') {
106
+ return executeInterrupt(state, scopeKey, current);
107
+ }
108
+ if (operation.kind === 'session.approval.respond') {
109
+ return executeApproval(state, scopeKey, current, payload);
110
+ }
111
+ return persist(state, scopeKey, transitionOperation(current, 'failed', { error: 'capability_unavailable' }));
112
+ }
113
+ catch {
114
+ return persist(state, scopeKey, transitionOperation(current, 'uncertain', { error: 'native_receipt_lost' }));
115
+ }
116
+ }
117
+ async function executeCreate(state, scopeKey, operation) {
118
+ const sessionId = required(operation.sessionId, 'sessionId');
119
+ const started = await native(state).startThread({
120
+ sessionId,
121
+ cwd: await workspaceRoot(state, operation.workspaceId),
122
+ });
123
+ if (!started.threadId) {
124
+ return persist(state, scopeKey, transitionOperation(operation, 'uncertain', { error: 'native_receipt_lost' }));
125
+ }
126
+ await state.journal.putSession({
127
+ sessionId,
128
+ workspaceId: operation.workspaceId,
129
+ threadId: started.threadId,
130
+ cwd: await workspaceRoot(state, operation.workspaceId),
131
+ recoverability: 'native',
132
+ });
133
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', {
134
+ nativeRequestId: `thread/start:${sessionId}`,
135
+ nativeThreadId: started.threadId,
136
+ }));
137
+ await recordNativeEvents(state, accepted, [
138
+ { type: 'session.created', payload: { sessionId, threadId: started.threadId }, occurredAt: new Date().toISOString() },
139
+ ]);
140
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed', { nativeThreadId: started.threadId }));
141
+ }
142
+ async function executeTurn(state, scopeKey, operation, payload) {
143
+ const sessionId = required(operation.sessionId, 'sessionId');
144
+ const binding = state.journal.getSession(sessionId);
145
+ if (!binding) {
146
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unknown_session' }));
147
+ }
148
+ if (binding.recoverability !== 'native') {
149
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'native_not_recoverable' }));
150
+ }
151
+ const body = asRecord(payload);
152
+ let result = await native(state).startTurn({
153
+ sessionId,
154
+ threadId: binding.threadId,
155
+ text: String(body.text ?? ''),
156
+ clientRequestId: operation.clientRequestId,
157
+ simulateLostReceipt: body.simulateLostReceipt === true,
158
+ });
159
+ if (result.outcome === 'uncertain') {
160
+ await recordNativeEvents(state, operation, result.events);
161
+ return persist(state, scopeKey, transitionOperation(operation, 'uncertain', {
162
+ error: 'native_receipt_lost',
163
+ nativeThreadId: binding.threadId,
164
+ }));
165
+ }
166
+ if (result.outcome === 'failed' || !result.turnId) {
167
+ await recordNativeEvents(state, operation, result.events);
168
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', {
169
+ error: 'native_failed',
170
+ nativeThreadId: binding.threadId,
171
+ }));
172
+ }
173
+ if (!result.approvalId &&
174
+ process.env.PLAYCRAFT_RECORD_PENDING_APPROVAL === '1' &&
175
+ String(body.text ?? '').includes('[approval]')) {
176
+ result = { ...result, approvalId: `approval_${result.turnId}` };
177
+ }
178
+ if (result.approvalId) {
179
+ await state.journal.putApproval({
180
+ approvalId: result.approvalId,
181
+ sessionId,
182
+ runtimeEpoch: state.runtimeEpoch,
183
+ nativeRequestId: `turn/start:${result.turnId}`,
184
+ status: 'pending',
185
+ });
186
+ }
187
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', {
188
+ nativeRequestId: `turn/start:${result.turnId}`,
189
+ nativeThreadId: binding.threadId,
190
+ nativeTurnId: result.turnId,
191
+ nativeApprovalId: result.approvalId,
192
+ }));
193
+ await recordNativeEvents(state, accepted, result.events);
194
+ const terminal = result.outcome === 'interrupted' ? 'interrupted' : 'completed';
195
+ return persist(state, scopeKey, transitionOperation(accepted, terminal, {
196
+ nativeTurnId: result.turnId,
197
+ nativeApprovalId: result.approvalId,
198
+ }));
199
+ }
200
+ async function executeInterrupt(state, scopeKey, operation) {
201
+ const sessionId = required(operation.sessionId, 'sessionId');
202
+ const binding = state.journal.getSession(sessionId);
203
+ if (!binding) {
204
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unknown_session' }));
205
+ }
206
+ const result = await native(state).interrupt({ sessionId, threadId: binding.threadId });
207
+ if (result.outcome === 'uncertain') {
208
+ return persist(state, scopeKey, transitionOperation(operation, 'uncertain', { error: 'native_receipt_lost' }));
209
+ }
210
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { nativeThreadId: binding.threadId }));
211
+ await recordNativeEvents(state, accepted, result.events);
212
+ return persist(state, scopeKey, transitionOperation(accepted, 'interrupted', { nativeThreadId: binding.threadId }));
213
+ }
214
+ async function executeApproval(state, scopeKey, operation, payload) {
215
+ const sessionId = required(operation.sessionId, 'sessionId');
216
+ const body = asRecord(payload);
217
+ const approvalId = String(body.approvalId ?? operation.nativeApprovalId ?? '');
218
+ if (!approvalId) {
219
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'missing_approval' }));
220
+ }
221
+ const stored = state.journal.getApproval(sessionId, approvalId);
222
+ if (stored && (stored.status === 'expired' || stored.runtimeEpoch !== state.runtimeEpoch)) {
223
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'approval_expired' }));
224
+ }
225
+ const result = await native(state).respondApproval({
226
+ sessionId,
227
+ approvalId,
228
+ accepted: body.accepted !== false,
229
+ });
230
+ if (stored && result.outcome !== 'failed') {
231
+ await state.journal.putApproval({
232
+ ...stored,
233
+ status: body.accepted === false ? 'rejected' : 'approved',
234
+ });
235
+ }
236
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { nativeApprovalId: approvalId }));
237
+ await recordNativeEvents(state, accepted, result.events);
238
+ const terminal = result.outcome === 'failed' ? 'failed' : 'completed';
239
+ return persist(state, scopeKey, transitionOperation(accepted, terminal, {
240
+ nativeApprovalId: approvalId,
241
+ error: terminal === 'failed' ? 'native_failed' : undefined,
242
+ }));
243
+ }
244
+ async function executeCheckpoint(state, scopeKey, operation, payload) {
245
+ const body = asRecord(payload);
246
+ const checkpointId = typeof body.checkpointId === 'string' && body.checkpointId.trim()
247
+ ? body.checkpointId.trim()
248
+ : `ckpt_${randomUUID()}`;
249
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(checkpointId) || state.journal.getCheckpoint(checkpointId)) {
250
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'checkpoint_id_conflict' }));
251
+ }
252
+ const workspaceId = operation.workspaceId ?? String(body.workspaceId ?? 'workspace');
253
+ const active = await activeManagedWrites(state, workspaceId);
254
+ const createdAt = new Date().toISOString();
255
+ if (active.length > 0) {
256
+ const sessions = workspaceSessions(state, operation.workspaceId).map((item) => ({
257
+ sessionId: item.sessionId,
258
+ nativeThreadId: item.threadId,
259
+ recoverability: 'history_only',
260
+ }));
261
+ const checkpoint = checkpointRecordSchema.parse({
262
+ checkpointId,
263
+ kind: 'best_effort',
264
+ workspaceId,
265
+ generation: state.config.generation,
266
+ runtimeEpoch: state.runtimeEpoch,
267
+ sessions,
268
+ createdAt,
269
+ nativeRecoverable: false,
270
+ consistent: false,
271
+ });
272
+ await state.journal.putCheckpoint(checkpoint);
273
+ return finishCheckpoint(state, scopeKey, operation, checkpoint, active.map((item) => item.source));
274
+ }
275
+ const root = await workspaceRoot(state, operation.workspaceId);
276
+ const revisions = state.journal.fileRevisions(revisionScope(state, operation.workspaceId));
277
+ let files;
278
+ let published;
279
+ if (state.config.fileObjectStoreUrl) {
280
+ const tree = await captureWorkspaceTree(root, previousFilePaths(state), revisions);
281
+ const stillActive = await activeManagedWrites(state, workspaceId);
282
+ if (stillActive.length > 0) {
283
+ return finishBestEffort(state, scopeKey, operation, checkpointId, workspaceId, createdAt, stillActive.map((item) => item.source));
284
+ }
285
+ try {
286
+ published = await publishFileSnapshot({
287
+ baseUrl: state.config.fileObjectStoreUrl,
288
+ snapshotId: checkpointId,
289
+ workspaceId,
290
+ generation: state.config.generation,
291
+ tree,
292
+ });
293
+ }
294
+ catch {
295
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'snapshot_upload_failed' }));
296
+ }
297
+ if (tree.status === 'incomplete') {
298
+ const sessions = workspaceSessions(state, operation.workspaceId).map((item) => ({
299
+ sessionId: item.sessionId,
300
+ nativeThreadId: item.threadId,
301
+ recoverability: 'history_only',
302
+ }));
303
+ const checkpoint = checkpointRecordSchema.parse({
304
+ checkpointId,
305
+ kind: 'file',
306
+ workspaceId,
307
+ generation: state.config.generation,
308
+ runtimeEpoch: state.runtimeEpoch,
309
+ sessions,
310
+ createdAt,
311
+ nativeRecoverable: false,
312
+ consistent: false,
313
+ manifestKey: published.manifestKey,
314
+ checksum: published.checksum,
315
+ snapshotStatus: 'incomplete',
316
+ });
317
+ await state.journal.putCheckpoint(checkpoint);
318
+ return finishCheckpoint(state, scopeKey, operation, checkpoint, []);
319
+ }
320
+ await materializeSnapshot(fileCheckpointDir(state, checkpointId), tree);
321
+ files = tree.files;
322
+ }
323
+ else {
324
+ files = await snapshotWorkspaceFiles(root, fileCheckpointDir(state, checkpointId), revisions);
325
+ const stillActive = await activeManagedWrites(state, workspaceId);
326
+ if (stillActive.length > 0) {
327
+ return finishBestEffort(state, scopeKey, operation, checkpointId, workspaceId, createdAt, stillActive.map((item) => item.source));
328
+ }
329
+ }
330
+ const captured = await captureNativeSessions(state, operation.workspaceId);
331
+ const nativeReady = captured.nativeReady;
332
+ const checkpoint = checkpointRecordSchema.parse({
333
+ checkpointId,
334
+ kind: nativeReady ? 'coordinated' : 'file',
335
+ workspaceId,
336
+ generation: state.config.generation,
337
+ runtimeEpoch: state.runtimeEpoch,
338
+ sessions: captured.sessions,
339
+ files,
340
+ createdAt,
341
+ nativeRecoverable: nativeReady,
342
+ consistent: nativeReady,
343
+ ...(published
344
+ ? { manifestKey: published.manifestKey, checksum: published.checksum, snapshotStatus: published.status }
345
+ : {}),
346
+ });
347
+ await state.journal.putCheckpoint(checkpoint);
348
+ await state.journal.setFileCheckpointId(checkpointId);
349
+ if (nativeReady)
350
+ await state.journal.setConsistentCheckpointId(checkpointId);
351
+ return finishCheckpoint(state, scopeKey, operation, checkpoint, []);
352
+ }
353
+ async function executeRestore(state, scopeKey, operation, payload) {
354
+ const body = asRecord(payload);
355
+ const checkpointId = String(body.checkpointId ?? operation.checkpointId ?? '');
356
+ const checkpoint = state.journal.getCheckpoint(checkpointId);
357
+ if (!checkpoint || checkpoint.workspaceId !== (operation.workspaceId ?? 'workspace')) {
358
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unknown_checkpoint' }));
359
+ }
360
+ if (checkpoint.snapshotStatus === 'incomplete') {
361
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'snapshot_incomplete' }));
362
+ }
363
+ const restarted = executionGroupRestarted(state, checkpoint.runtimeEpoch);
364
+ const stopConfirmed = state.journal.executionGroup().stopConfirmed;
365
+ const cwd = await workspaceRoot(state, operation.workspaceId);
366
+ const next = workspaceSessions(state, operation.workspaceId).map((item) => {
367
+ const snap = checkpoint.sessions.find((session) => session.sessionId === item.sessionId);
368
+ if (!snap)
369
+ return { ...item, recoverability: 'history_only' };
370
+ const threadId = item.threadId;
371
+ const threadReplaced = Boolean(snap.nativeThreadId && snap.nativeThreadId !== item.threadId);
372
+ if (!checkpoint.nativeRecoverable || threadReplaced) {
373
+ return {
374
+ ...item,
375
+ threadId,
376
+ recoverability: restarted ? 'history_only' : item.recoverability,
377
+ };
378
+ }
379
+ if (restarted)
380
+ return { ...item, threadId, recoverability: 'history_only' };
381
+ return { ...item, threadId: snap.nativeThreadId ?? item.threadId, recoverability: 'native' };
382
+ });
383
+ for (const snap of checkpoint.sessions) {
384
+ if (!next.some((item) => item.sessionId === snap.sessionId)) {
385
+ next.push({
386
+ sessionId: snap.sessionId,
387
+ workspaceId: operation.workspaceId,
388
+ threadId: snap.nativeThreadId ?? `thread_${snap.sessionId}`,
389
+ cwd,
390
+ recoverability: checkpoint.nativeRecoverable && !restarted ? 'native' : 'history_only',
391
+ });
392
+ }
393
+ }
394
+ let nativeContinuable = checkpoint.nativeRecoverable
395
+ ? next.some((item) => item.recoverability === 'native')
396
+ : false;
397
+ if (checkpoint.nativeRecoverable && restarted && stopConfirmed) {
398
+ for (const snap of checkpoint.sessions) {
399
+ const threadId = snap.nativeThreadId;
400
+ const found = next.find((item) => item.sessionId === snap.sessionId);
401
+ if (!threadId || !found || !state.native?.canContinueThread)
402
+ continue;
403
+ const continued = await state.native.canContinueThread({
404
+ threadId,
405
+ sessionId: snap.sessionId,
406
+ cwd: found.cwd,
407
+ });
408
+ if (!continued || found.threadId !== threadId)
409
+ continue;
410
+ found.recoverability = 'native';
411
+ nativeContinuable = true;
412
+ }
413
+ }
414
+ if (!checkpoint.nativeRecoverable || (restarted && !stopConfirmed))
415
+ nativeContinuable = false;
416
+ const others = state.config.workspaceMode === 'multi'
417
+ ? state.journal.listSessions().filter((item) => item.workspaceId !== operation.workspaceId) : [];
418
+ if (others.some((item) => next.some((restored) => restored.sessionId === item.sessionId))) {
419
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'session_workspace_mismatch' }));
420
+ }
421
+ await state.journal.replaceSessions([...others, ...next]);
422
+ let filesRestored = false;
423
+ if (checkpoint.files) {
424
+ const restoredFiles = await restoreWorkspaceFiles(cwd, fileCheckpointDir(state, checkpointId), checkpoint.files);
425
+ await state.journal.replaceFileRevisions(restoredFiles, revisionScope(state, operation.workspaceId));
426
+ filesRestored = true;
427
+ }
428
+ const historyVisible = state.journal.getCheckpoint(checkpointId) !== undefined &&
429
+ state.journal.listEvents().some((event) => event.type === 'checkpoint.created' || event.type === 'session.created');
430
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { checkpointId }));
431
+ await recordNativeEvents(state, accepted, [
432
+ {
433
+ type: 'checkpoint.restored',
434
+ payload: {
435
+ checkpointId,
436
+ historyVisible,
437
+ filesRestored,
438
+ nativeContinuable,
439
+ nativeSessionCount: next.filter((item) => item.recoverability === 'native').length,
440
+ historyOnlyCount: next.filter((item) => item.recoverability === 'history_only').length,
441
+ },
442
+ occurredAt: new Date().toISOString(),
443
+ },
444
+ ]);
445
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed', { checkpointId }));
446
+ }
447
+ async function executeDestroy(state, scopeKey, operation) {
448
+ await state.journal.markDestroyed();
449
+ try {
450
+ await state.native?.close?.();
451
+ }
452
+ catch {
453
+ /* Codex process shutdown is best-effort once the runtime is destroyed */
454
+ }
455
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
456
+ await recordNativeEvents(state, accepted, [
457
+ {
458
+ type: 'runtime.destroyed',
459
+ payload: { runtimeId: state.config.runtimeId },
460
+ occurredAt: new Date().toISOString(),
461
+ },
462
+ ]);
463
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed'));
464
+ }
465
+ async function executeFileWrite(state, scopeKey, operation, payload) {
466
+ const parsed = fileWritePayloadSchema.safeParse(payload);
467
+ if (!parsed.success) {
468
+ const message = parsed.error.issues[0]?.message;
469
+ const error = message === 'write_id_required' || message === 'write_hold_conflict' || message === 'background_write_unmanaged'
470
+ ? message
471
+ : 'invalid_workspace_path';
472
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error }));
473
+ }
474
+ const workspaceId = operation.workspaceId ?? 'workspace';
475
+ const writer = parsed.data.writer ?? 'manual';
476
+ if (parsed.data.release) {
477
+ const lease = state.journal.getManagedWrite(parsed.data.writeId);
478
+ if (!lease || lease.stoppedAt || lease.workspaceId !== workspaceId) {
479
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'write_unknown' }));
480
+ }
481
+ }
482
+ if (parsed.data.hold) {
483
+ const existing = state.journal.getManagedWrite(parsed.data.writeId);
484
+ if (existing && !existing.stoppedAt) {
485
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'write_still_active' }));
486
+ }
487
+ }
488
+ if (parsed.data.release) {
489
+ const stopped = await releaseManagedWrite(state, parsed.data.writeId, workspaceId);
490
+ if (!stopped) {
491
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'write_not_stopped' }));
492
+ }
493
+ }
494
+ const expected = parsed.data.expectedRevision ?? operation.expectedRevision;
495
+ return withObservedFile(state, operation.workspaceId, parsed.data.path, async ({ revision: currentRevision }) => {
496
+ if (expected !== undefined && expected !== currentRevision) {
497
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'revision_conflict' }));
498
+ }
499
+ try {
500
+ const root = await workspaceRoot(state, operation.workspaceId);
501
+ const written = await writeWorkspaceFile(root, parsed.data.path, parsed.data.content);
502
+ const revision = currentRevision + 1;
503
+ await state.journal.putFileRevision(parsed.data.path, {
504
+ revision,
505
+ contentHash: written.contentHash,
506
+ }, revisionScope(state, operation.workspaceId));
507
+ if (parsed.data.hold && parsed.data.writeId) {
508
+ let pid;
509
+ if (writer === 'background') {
510
+ pid = spawnBackgroundWriter(await resolveWorkspaceFile(root, parsed.data.path));
511
+ }
512
+ await state.journal.putManagedWrite({
513
+ writeId: parsed.data.writeId,
514
+ workspaceId,
515
+ source: writer,
516
+ startedAt: new Date().toISOString(),
517
+ pid,
518
+ });
519
+ }
520
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { expectedRevision: revision }));
521
+ await recordNativeEvents(state, accepted, [
522
+ {
523
+ type: 'file.written',
524
+ payload: {
525
+ path: parsed.data.path,
526
+ revision,
527
+ contentHash: written.contentHash,
528
+ size: written.size,
529
+ writer,
530
+ writeId: parsed.data.writeId,
531
+ },
532
+ occurredAt: new Date().toISOString(),
533
+ },
534
+ ]);
535
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed', { expectedRevision: revision }));
536
+ }
537
+ catch (error) {
538
+ const code = error instanceof InvalidWorkspacePathError ? 'invalid_workspace_path' : 'file_write_failed';
539
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: code }));
540
+ }
541
+ }).catch((error) => {
542
+ const code = error instanceof InvalidWorkspacePathError ? 'invalid_workspace_path' : 'file_write_failed';
543
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: code }));
544
+ });
545
+ }
546
+ async function executeGitCommit(state, scopeKey, operation, payload) {
547
+ const parsed = gitCommitPayloadSchema.safeParse(payload);
548
+ if (!parsed.success) {
549
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_git_commit' }));
550
+ }
551
+ try {
552
+ const sha = await commitWorkspaceGit(await workspaceRoot(state, operation.workspaceId), parsed.data);
553
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
554
+ await recordNativeEvents(state, accepted, [
555
+ {
556
+ type: 'git.committed',
557
+ payload: { sha, message: parsed.data.message, paths: parsed.data.paths ?? [] },
558
+ occurredAt: new Date().toISOString(),
559
+ },
560
+ ]);
561
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed'));
562
+ }
563
+ catch (error) {
564
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: gitErrorCode(error) }));
565
+ }
566
+ }
567
+ async function executeGitPush(state, scopeKey, operation, payload) {
568
+ const parsed = gitPushPayloadSchema.safeParse(payload ?? {});
569
+ if (!parsed.success) {
570
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'remote_not_configurable' }));
571
+ }
572
+ try {
573
+ await pushWorkspaceGit(await workspaceRoot(state, operation.workspaceId));
574
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
575
+ await recordNativeEvents(state, accepted, [
576
+ {
577
+ type: 'git.pushed',
578
+ payload: { remote: 'origin' },
579
+ occurredAt: new Date().toISOString(),
580
+ },
581
+ ]);
582
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed'));
583
+ }
584
+ catch (error) {
585
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: gitErrorCode(error) }));
586
+ }
587
+ }
588
+ async function executeParameterPatch(state, scopeKey, operation, payload) {
589
+ const parsed = parameterPatchPayloadSchema.safeParse(payload);
590
+ if (!parsed.success) {
591
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_parameter_patch' }));
592
+ }
593
+ const root = await workspaceRoot(state, operation.workspaceId);
594
+ const scope = revisionScope(state, operation.workspaceId);
595
+ const document = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
596
+ if (document.status === 'missing' || document.status === 'schema_only' || !document.dataPath) {
597
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_data_missing' }));
598
+ }
599
+ if (document.status === 'invalid') {
600
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_document_invalid' }));
601
+ }
602
+ const dataPath = document.dataPath;
603
+ return withObservedFile(state, operation.workspaceId, dataPath, async ({ revision: currentRevision }) => {
604
+ if (parsed.data.expectedRevision !== currentRevision) {
605
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'revision_conflict' }));
606
+ }
607
+ try {
608
+ const written = await patchWorkspaceParameters(root, state.journal.fileRevisions(scope), parsed.data);
609
+ const revision = currentRevision + 1;
610
+ await state.journal.putFileRevision(dataPath, { revision, contentHash: written.contentHash }, scope);
611
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { expectedRevision: revision }));
612
+ await recordNativeEvents(state, accepted, [
613
+ {
614
+ type: 'file.written',
615
+ payload: {
616
+ path: dataPath,
617
+ revision,
618
+ contentHash: written.contentHash,
619
+ size: written.size,
620
+ fieldPath: parsed.data.fieldPath,
621
+ },
622
+ occurredAt: new Date().toISOString(),
623
+ },
624
+ ]);
625
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed', { expectedRevision: revision }));
626
+ }
627
+ catch (error) {
628
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: parameterPatchError(error) }));
629
+ }
630
+ }).catch((error) => persist(state, scopeKey, transitionOperation(operation, 'failed', { error: parameterPatchError(error) })));
631
+ }
632
+ async function executeTerminalCreate(state, scopeKey, operation, payload) {
633
+ const parsed = terminalCreatePayloadSchema.safeParse(payload ?? {});
634
+ if (!parsed.success) {
635
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_terminal_create' }));
636
+ }
637
+ const workspaceId = operation.workspaceId;
638
+ if (!workspaceId) {
639
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_terminal_create' }));
640
+ }
641
+ const terminalId = parsed.data.terminalId ?? operation.terminalId;
642
+ if (!terminalId) {
643
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_terminal_create' }));
644
+ }
645
+ const existing = state.journal.getTerminal(terminalId);
646
+ if (existing && existing.workspaceId !== workspaceId) {
647
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'terminal_already_bound' }));
648
+ }
649
+ if (existing?.status === 'destroyed') {
650
+ return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'terminal_destroyed' }));
651
+ }
652
+ const cwd = parsed.data.cwd ?? existing?.cwd ?? '';
653
+ const createdAt = existing?.createdAt ?? new Date().toISOString();
654
+ const record = await state.journal.putTerminal({
655
+ terminalId,
656
+ workspaceId,
657
+ environment: state.config.environment,
658
+ userId: state.config.userId,
659
+ runtimeId: state.config.runtimeId,
660
+ generation: state.config.generation,
661
+ runtimeEpoch: state.runtimeEpoch,
662
+ processIdentity: terminalProcessIdentity(workspaceId, terminalId, cwd),
663
+ cwd,
664
+ status: 'active',
665
+ createdAt,
666
+ });
667
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { terminalId, cwd: record.cwd }));
668
+ await recordNativeEvents(state, accepted, [
669
+ {
670
+ type: 'terminal.created',
671
+ payload: { terminalId, cwd: record.cwd, processIdentity: record.processIdentity },
672
+ occurredAt: new Date().toISOString(),
673
+ },
674
+ ]);
675
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed', { terminalId, cwd: record.cwd }));
676
+ }
677
+ function gitErrorCode(error) {
678
+ return error instanceof GitWorkspaceError ? error.code : 'git_command_failed';
679
+ }
680
+ function parameterPatchError(error) {
681
+ if (error instanceof ParameterEditError)
682
+ return error.code;
683
+ if (error instanceof InvalidWorkspacePathError)
684
+ return 'invalid_workspace_path';
685
+ return 'parameter_patch_failed';
686
+ }
687
+ async function finishCheckpoint(state, scopeKey, operation, checkpoint, activeWriters) {
688
+ const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { checkpointId: checkpoint.checkpointId }));
689
+ await recordNativeEvents(state, accepted, [
690
+ {
691
+ type: 'checkpoint.created',
692
+ payload: {
693
+ checkpointId: checkpoint.checkpointId,
694
+ workspaceId: checkpoint.workspaceId,
695
+ kind: checkpoint.kind,
696
+ consistent: checkpoint.consistent === true,
697
+ fileCheckpointId: state.journal.fileCheckpointId(),
698
+ consistentCheckpointId: state.journal.consistentCheckpointId(),
699
+ nativeRecoverable: checkpoint.nativeRecoverable,
700
+ sessionCount: checkpoint.sessions.length,
701
+ fileCount: checkpoint.files?.length ?? 0,
702
+ activeWriters,
703
+ },
704
+ occurredAt: checkpoint.createdAt,
705
+ },
706
+ ]);
707
+ return persist(state, scopeKey, transitionOperation(accepted, 'completed', {
708
+ checkpointId: checkpoint.checkpointId,
709
+ checkpointKind: checkpoint.kind,
710
+ checkpointConsistent: checkpoint.consistent === true,
711
+ ...(checkpoint.manifestKey ? { snapshotManifestKey: checkpoint.manifestKey } : {}),
712
+ ...(checkpoint.checksum ? { snapshotChecksum: checkpoint.checksum } : {}),
713
+ ...(checkpoint.snapshotStatus ? { snapshotStatus: checkpoint.snapshotStatus } : {}),
714
+ }));
715
+ }
716
+ async function captureNativeSessions(state, workspaceId) {
717
+ const live = workspaceSessions(state, workspaceId);
718
+ const sessions = [];
719
+ let nativeReady = live.length > 0;
720
+ for (const item of live) {
721
+ const captured = await state.native?.captureNativeCheckpoint?.({
722
+ threadId: item.threadId,
723
+ sessionId: item.sessionId,
724
+ });
725
+ const durable = captured?.threadId === item.threadId && typeof captured.nativeDurableTurnId === 'string'
726
+ ? captured.nativeDurableTurnId
727
+ : undefined;
728
+ if (!durable)
729
+ nativeReady = false;
730
+ sessions.push(durable
731
+ ? {
732
+ sessionId: item.sessionId,
733
+ nativeThreadId: item.threadId,
734
+ nativeDurableTurnId: durable,
735
+ recoverability: 'native',
736
+ }
737
+ : {
738
+ sessionId: item.sessionId,
739
+ nativeThreadId: item.threadId,
740
+ recoverability: 'history_only',
741
+ });
742
+ }
743
+ return { sessions, nativeReady };
744
+ }
745
+ async function activeManagedWrites(state, workspaceId) {
746
+ const active = [];
747
+ for (const lease of state.journal.listManagedWrites(workspaceId)) {
748
+ if (lease.stoppedAt)
749
+ continue;
750
+ if (lease.pid !== undefined && probeProcess(lease.pid) === false) {
751
+ await state.journal.putManagedWrite({ ...lease, stoppedAt: new Date().toISOString() });
752
+ continue;
753
+ }
754
+ active.push(lease);
755
+ }
756
+ return active;
757
+ }
758
+ async function releaseManagedWrite(state, writeId, workspaceId) {
759
+ const lease = state.journal.getManagedWrite(writeId);
760
+ if (!lease || lease.stoppedAt || lease.workspaceId !== workspaceId)
761
+ return false;
762
+ if (lease.pid !== undefined) {
763
+ const stopped = await stopBackgroundWriter(lease.pid);
764
+ if (!stopped)
765
+ return false;
766
+ }
767
+ await state.journal.putManagedWrite({ ...lease, stoppedAt: new Date().toISOString() });
768
+ return true;
769
+ }
770
+ async function finishBestEffort(state, scopeKey, operation, checkpointId, workspaceId, createdAt, activeWriters) {
771
+ const sessions = workspaceSessions(state, operation.workspaceId).map((item) => ({
772
+ sessionId: item.sessionId,
773
+ nativeThreadId: item.threadId,
774
+ recoverability: 'history_only',
775
+ }));
776
+ const checkpoint = checkpointRecordSchema.parse({
777
+ checkpointId,
778
+ kind: 'best_effort',
779
+ workspaceId,
780
+ generation: state.config.generation,
781
+ runtimeEpoch: state.runtimeEpoch,
782
+ sessions,
783
+ createdAt,
784
+ nativeRecoverable: false,
785
+ consistent: false,
786
+ });
787
+ await state.journal.putCheckpoint(checkpoint);
788
+ return finishCheckpoint(state, scopeKey, operation, checkpoint, activeWriters);
789
+ }
790
+ function previousFilePaths(state) {
791
+ const checkpointId = state.journal.fileCheckpointId();
792
+ if (!checkpointId)
793
+ return [];
794
+ return state.journal.getCheckpoint(checkpointId)?.files?.map((file) => file.path) ?? [];
795
+ }
796
+ function fileCheckpointDir(state, checkpointId) {
797
+ return join(state.config.stateDir, 'file-checkpoints', checkpointId);
798
+ }
799
+ export async function recordNativeEvents(state, operation, events) {
800
+ const recorded = [];
801
+ for (const event of events) {
802
+ const sourceSequence = nextSourceSequence(state.journal.lastSourceSequence());
803
+ const payload = {
804
+ ...(asRecord(event.payload)),
805
+ ...(operation.sessionId ? { sessionId: operation.sessionId } : {}),
806
+ ...(operation.workspaceId ? { workspaceId: operation.workspaceId } : {}),
807
+ };
808
+ const pending = pendingSourceEventSchema.parse({
809
+ source: {
810
+ runtimeId: state.config.runtimeId,
811
+ generation: state.config.generation,
812
+ runtimeEpoch: state.runtimeEpoch,
813
+ sourceSequence,
814
+ },
815
+ payloadHash: hashOperationPayload(payload),
816
+ type: event.type,
817
+ payload,
818
+ occurredAt: event.occurredAt,
819
+ });
820
+ recorded.push(await state.journal.appendEvent(pending));
821
+ }
822
+ if (recorded.length > 0) {
823
+ await notifyCallback(state, recorded).catch(() => undefined);
824
+ }
825
+ void operation;
826
+ return recorded;
827
+ }
828
+ async function notifyCallback(state, events) {
829
+ await fetch(state.config.callbackUrl, {
830
+ method: 'POST',
831
+ headers: {
832
+ authorization: `Bearer ${state.config.controlToken}`,
833
+ 'x-playcraft-control-token': state.config.controlToken,
834
+ 'content-type': 'application/json',
835
+ },
836
+ body: JSON.stringify({
837
+ runtimeId: state.config.runtimeId,
838
+ generation: state.config.generation,
839
+ runtimeEpoch: state.runtimeEpoch,
840
+ events,
841
+ }),
842
+ });
843
+ }
844
+ async function persist(state, scopeKey, operation) {
845
+ return state.journal.putOperation(scopeKey, operation);
846
+ }
847
+ function native(state) {
848
+ if (!state.native)
849
+ throw new Error('native_adapter_missing');
850
+ return state.native;
851
+ }
852
+ function required(value, name) {
853
+ if (!value)
854
+ throw new Error(`missing_${name}`);
855
+ return value;
856
+ }
857
+ function asRecord(payload) {
858
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
859
+ ? payload
860
+ : {};
861
+ }