@playcraft/cli 0.1.57-beta → 0.1.58-beta

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 (82) hide show
  1. package/dist/commands/workspace.js +43 -0
  2. package/dist/workspace/messages.js +10 -0
  3. package/dist/workspace-runtime/batch/deliver.js +140 -80
  4. package/dist/workspace-runtime/batch/freeze.js +18 -6
  5. package/dist/workspace-runtime/batch/generate.js +68 -64
  6. package/dist/workspace-runtime/batch/prepare.js +108 -93
  7. package/dist/workspace-runtime/batch/records.js +4 -0
  8. package/dist/workspace-runtime/batch/routes.js +10 -2
  9. package/dist/workspace-runtime/batch/source-candidate.js +2 -0
  10. package/dist/workspace-runtime/batch/source-capture.js +1 -0
  11. package/dist/workspace-runtime/batch/source-failure.js +2 -0
  12. package/dist/workspace-runtime/batch/source-publish.js +3 -0
  13. package/dist/workspace-runtime/batch/writer-proof.js +37 -0
  14. package/dist/workspace-runtime/codex/app-server.js +1 -0
  15. package/dist/workspace-runtime/codex/config-toml.js +4 -0
  16. package/dist/workspace-runtime/codex/delivery-tools.js +1 -1
  17. package/dist/workspace-runtime/persistence/journal.js +34 -0
  18. package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
  19. package/dist/workspace-runtime/previews/output.js +2 -0
  20. package/dist/workspace-runtime/previews/publish.js +20 -5
  21. package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
  22. package/dist/workspace-runtime/processes/managed-writes.js +47 -5
  23. package/dist/workspace-runtime/serve.js +11 -2
  24. package/dist/workspace-runtime/server/accept-operation.js +22 -2
  25. package/dist/workspace-runtime/server/directory-access.js +414 -0
  26. package/dist/workspace-runtime/server/dispatch.js +61 -23
  27. package/dist/workspace-runtime/server/endpoints.js +1 -0
  28. package/dist/workspace-runtime/server/http.js +52 -10
  29. package/dist/workspace-runtime/server/operation-record.js +2 -0
  30. package/dist/workspace-runtime/server/terminal-host.js +43 -8
  31. package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
  32. package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
  33. package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
  34. package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
  35. package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
  36. package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
  37. package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
  38. package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
  39. package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
  40. package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
  41. package/dist/workspace-runtime/workspaces/attachments.js +9 -0
  42. package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
  43. package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
  44. package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
  45. package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
  46. package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
  47. package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
  48. package/dist/workspace-runtime/workspaces/dependencies.js +90 -70
  49. package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
  50. package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
  51. package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
  52. package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
  53. package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
  54. package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
  55. package/dist/workspace-runtime/workspaces/files.js +4 -0
  56. package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
  57. package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
  58. package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
  59. package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
  60. package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
  61. package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
  62. package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
  63. package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
  64. package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
  65. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
  66. package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
  67. package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
  68. package/dist/workspace-runtime/workspaces/parameters.js +95 -3
  69. package/dist/workspace-runtime/workspaces/platform-export.js +5 -0
  70. package/dist/workspace-runtime/workspaces/prepare.js +4 -0
  71. package/dist/workspace-runtime/workspaces/registry.js +11 -0
  72. package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
  73. package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
  74. package/dist/workspace-runtime/workspaces/skills.js +14 -15
  75. package/dist/workspace-runtime/workspaces/transfer.js +4 -1
  76. package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
  77. package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
  78. package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
  79. package/package.json +5 -4
  80. package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
  81. package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
  82. package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
@@ -0,0 +1,235 @@
1
+ import { lstat, readdir } from 'node:fs/promises';
2
+ import { dirname, join, relative as relativePath } from 'node:path';
3
+ import { runDeveloperCommand } from './developer-command.js';
4
+ import { SHARED_DIRECTORY_MODE, SHARED_FILE_MODE, sharePathWithGroup, workspaceGroup, } from './shared-write.js';
5
+ /**
6
+ * Subtrees both sandbox identities read and write. Repair never walks the whole checkout: source
7
+ * files belong to Git and are opened once during preparation; these are the trees a session keeps
8
+ * writing after that point.
9
+ */
10
+ export const WORKSPACE_SHARED_PATHS = [
11
+ '.playcraft/artifact-receipts',
12
+ '.playcraft/skills',
13
+ '.playcraft/skill-artifacts',
14
+ '.playcraft-temp',
15
+ 'dist',
16
+ ];
17
+ /** Only the root is shared here; the transaction directories below it stay private. */
18
+ const SHARED_ROOT_ONLY = new Set(['.playcraft-temp']);
19
+ const PERMISSION_CODES = new Set(['EACCES', 'EPERM']);
20
+ export function isWorkspacePermissionError(error) {
21
+ const code = error?.code;
22
+ return typeof code === 'string' && PERMISSION_CODES.has(code);
23
+ }
24
+ /** Recognises a filesystem EACCES/EPERM, unwrapping the `cause` some guards attach. */
25
+ export function permissionDriftFromError(error) {
26
+ const seen = error instanceof Error && error.cause ? error.cause : error;
27
+ const node = seen;
28
+ if (!isWorkspacePermissionError(node) || typeof node.path !== 'string' || !node.path)
29
+ return undefined;
30
+ const search = ['scandir', 'readdir', 'stat', 'access', 'opendir'].includes(String(node.syscall));
31
+ return { path: node.path, missing: search ? 'group_search' : 'group_write' };
32
+ }
33
+ /** Recognises the EACCES/EPERM line a failed command or scan leaves in its log. */
34
+ export function permissionDriftFromOutput(output) {
35
+ const match = /(?:EACCES|EPERM): permission denied, (stat|scandir|open|access|readdir|mkdir|unlink|rename) '([^']+)'/
36
+ .exec(output);
37
+ if (!match)
38
+ return undefined;
39
+ const operation = match[1];
40
+ return {
41
+ path: match[2],
42
+ missing: operation === 'scandir' || operation === 'readdir' || operation === 'stat' || operation === 'access'
43
+ ? 'group_search'
44
+ : 'group_write',
45
+ };
46
+ }
47
+ function isMissing(error) {
48
+ return error?.code === 'ENOENT';
49
+ }
50
+ async function shapeOwnedTree(path, groupId, recursive = true) {
51
+ const stat = await lstat(path);
52
+ if (stat.isSymbolicLink())
53
+ return { repaired: 0, foreign: false };
54
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
55
+ // The other identity owns this inode; its own pass repairs the subtree.
56
+ if (uid !== undefined && uid !== 0 && stat.uid !== uid)
57
+ return { repaired: 0, foreign: true };
58
+ const changed = await sharePathWithGroup(path, groupId, stat.isDirectory() ? SHARED_DIRECTORY_MODE : SHARED_FILE_MODE, stat.isFile());
59
+ let repaired = changed ? 1 : 0;
60
+ let foreign = false;
61
+ if (!stat.isDirectory() || !recursive)
62
+ return { repaired, foreign };
63
+ const entries = await readdir(path).catch(error => {
64
+ // An unreadable directory can only be opened by its owner.
65
+ if (isWorkspacePermissionError(error)) {
66
+ foreign = true;
67
+ return [];
68
+ }
69
+ throw error;
70
+ });
71
+ for (const entry of entries) {
72
+ const result = await shapeOwnedTree(join(path, entry), groupId);
73
+ repaired += result.repaired;
74
+ foreign ||= result.foreign;
75
+ }
76
+ return { repaired, foreign };
77
+ }
78
+ function missingShare(stat, groupId) {
79
+ if (stat.gid !== groupId)
80
+ return 'group_write';
81
+ const mode = Number(stat.mode);
82
+ if (stat.isDirectory()) {
83
+ if ((mode & 0o010) === 0)
84
+ return 'group_search';
85
+ if ((mode & 0o060) !== 0o060 || (mode & 0o2000) === 0)
86
+ return 'group_write';
87
+ }
88
+ else if ((mode & 0o020) === 0)
89
+ return 'group_write';
90
+ return undefined;
91
+ }
92
+ /**
93
+ * Lists entries of the shared subtrees that lost development-group access. Read-only: the owner of
94
+ * each entry (or `repairWorkspaceShares` at registration) is what changes them.
95
+ */
96
+ export async function diagnoseWorkspaceShares(root, scope = 'workspace') {
97
+ const groupId = await workspaceGroup(root);
98
+ const targets = scope === 'workspace'
99
+ ? WORKSPACE_SHARED_PATHS.map((path) => join(root, path))
100
+ : [scope];
101
+ const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
102
+ const drifts = [];
103
+ const visit = async (path, recursive) => {
104
+ const stat = await lstat(path);
105
+ if (stat.isSymbolicLink())
106
+ return;
107
+ const missing = missingShare(stat, groupId);
108
+ if (missing)
109
+ drifts.push({ path, missing, owner: uid !== undefined && stat.uid === uid ? 'self' : 'other' });
110
+ if (!stat.isDirectory() || !recursive)
111
+ return;
112
+ const entries = await readdir(path).catch(error => {
113
+ if (isWorkspacePermissionError(error))
114
+ return [];
115
+ throw error;
116
+ });
117
+ for (const entry of entries)
118
+ await visit(join(path, entry), true);
119
+ };
120
+ for (const target of targets) {
121
+ const stat = await lstat(target).catch(error => { if (isMissing(error))
122
+ return undefined; throw error; });
123
+ if (!stat)
124
+ continue;
125
+ await visit(target, !SHARED_ROOT_ONLY.has(relativePath(root, target)));
126
+ }
127
+ return drifts;
128
+ }
129
+ function shellQuote(value) {
130
+ return `'${value.replace(/'/g, `'\\''`)}'`;
131
+ }
132
+ /**
133
+ * The launcher runs commands as the development identity. Entries that identity owns cannot be
134
+ * re-shaped from the controller, so the same invariants are applied from the owning side.
135
+ */
136
+ async function repairAsOwner(state, root, targets, groupId) {
137
+ if (!targets.length)
138
+ return 0;
139
+ const command = [
140
+ 'set -u',
141
+ 'count=0',
142
+ ...targets.map((target) => {
143
+ const quoted = shellQuote(target);
144
+ return [
145
+ `if test -e ${quoted}; then`,
146
+ ` n=$(find ${quoted} -uid "$(id -u)" \\( -type d ! -perm -2770 -o -type f ! -perm -g+w \\) -print 2>/dev/null | wc -l)`,
147
+ ' count=$((count+n))',
148
+ ` find ${quoted} -uid "$(id -u)" -type d ! -perm -2770 -exec chmod 2770 {} + 2>/dev/null || true`,
149
+ ` find ${quoted} -uid "$(id -u)" -type f ! -perm -g+w -exec chmod g+rw {} + 2>/dev/null || true`,
150
+ ` find ${quoted} -uid "$(id -u)" ! -group ${groupId} -exec chgrp ${groupId} {} + 2>/dev/null || true`,
151
+ 'fi',
152
+ ].join('\n');
153
+ }),
154
+ 'printf "repaired=%s\\n" "$count"',
155
+ ].join('\n');
156
+ let repaired = 0;
157
+ await runDeveloperCommand(state, {
158
+ processId: `share-repair-${Date.now().toString(36)}`,
159
+ // The target itself may be unreadable to the controller; the workspace root is not.
160
+ cwd: root,
161
+ command,
162
+ timeoutMs: 30_000,
163
+ onOutput: (chunk) => {
164
+ const found = /repaired=(\d+)/.exec(String(chunk));
165
+ if (found)
166
+ repaired = Math.max(repaired, Number(found[1]));
167
+ },
168
+ });
169
+ return repaired;
170
+ }
171
+ /**
172
+ * Re-applies the shared mode to the subtrees both identities use. Idempotent: only entries missing
173
+ * group access are touched. `scope` names one path when a failure already identified the drift.
174
+ */
175
+ export async function repairWorkspaceShares(state, root, scope = 'workspace') {
176
+ const groupId = await workspaceGroup(root);
177
+ const targets = scope === 'workspace'
178
+ ? WORKSPACE_SHARED_PATHS.map((path) => ({
179
+ path: join(root, path),
180
+ recursive: !SHARED_ROOT_ONLY.has(path),
181
+ }))
182
+ : [{ path: scope, recursive: true }];
183
+ const existing = [];
184
+ let repaired = 0;
185
+ const foreign = [];
186
+ for (const target of targets) {
187
+ const stat = await lstat(target.path).catch(error => { if (isMissing(error))
188
+ return undefined; throw error; });
189
+ if (!stat || !stat.isDirectory() || stat.isSymbolicLink())
190
+ continue;
191
+ existing.push(target.path);
192
+ const result = await shapeOwnedTree(target.path, groupId, target.recursive);
193
+ repaired += result.repaired;
194
+ if (result.foreign)
195
+ foreign.push(target.path);
196
+ }
197
+ if (foreign.length)
198
+ repaired += await repairAsOwner(state, root, foreign, groupId);
199
+ const report = { scope, repaired, paths: existing, delegated: foreign.length > 0 };
200
+ if (repaired > 0 || foreign.length > 0) {
201
+ console.log(JSON.stringify({
202
+ event: 'workspace_share_repaired',
203
+ scope,
204
+ root,
205
+ repaired,
206
+ paths: existing.map((path) => (scope === 'workspace' ? path.slice(root.length + 1) : path)),
207
+ delegated: report.delegated,
208
+ }));
209
+ }
210
+ return report;
211
+ }
212
+ /** Repairs once and retries a read/build that failed only because of a shared-permission drift. */
213
+ export async function withShareRepair(state, root, action, drift) {
214
+ try {
215
+ return await action();
216
+ }
217
+ catch (error) {
218
+ if (!isWorkspacePermissionError(error))
219
+ throw error;
220
+ const path = drift?.path ?? permissionDriftFromError(error)?.path;
221
+ await repairWorkspaceShares(state, root, await driftScope(root, path));
222
+ return action();
223
+ }
224
+ }
225
+ /** The shared subtree (or explicit path) that contains a drifting path. */
226
+ export async function driftScope(root, path) {
227
+ if (!path)
228
+ return 'workspace';
229
+ for (const relative of WORKSPACE_SHARED_PATHS) {
230
+ const candidate = join(root, relative);
231
+ if (path === candidate || path.startsWith(`${candidate}/`))
232
+ return candidate;
233
+ }
234
+ return dirname(path);
235
+ }
@@ -11,6 +11,7 @@ async function safeParents(root, path, create = false) {
11
11
  if (!segment || segment === '.' || segment === '..')
12
12
  throw new Error('workspace_worktree_attachment_unavailable');
13
13
  current = join(current, segment);
14
+ // shared
14
15
  if (create)
15
16
  await mkdir(current, { mode: 0o755 }).catch(error => { if (error.code !== 'EEXIST')
16
17
  throw error; });
@@ -43,6 +44,7 @@ async function copyExact(source, target, path, limit) {
43
44
  return;
44
45
  }
45
46
  const temporary = `${join(target, path)}.${randomUUID()}.tmp`;
47
+ // shared
46
48
  try {
47
49
  await writeFile(temporary, body, { flag: 'wx', mode: 0o644 });
48
50
  await rename(temporary, join(target, path));
@@ -1,22 +1,27 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { constants } from 'node:fs';
4
- import { lstat, mkdir, open, realpath, writeFile } from 'node:fs/promises';
4
+ import { lstat, open, realpath } from 'node:fs/promises';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { promisify } from 'node:util';
7
7
  import { validateProjectSkillsSnapshot } from '@playcraft/common/project-skills';
8
8
  import { readWorkspaceSkills } from './skills.js';
9
+ import { ensureSharedDirectory, writeSharedFile } from './shared-write.js';
9
10
  const execute = promisify(execFile);
10
11
  async function archiveDirectory(root) {
11
12
  let directory = root;
12
13
  for (const segment of ['.playcraft', 'skill-artifacts']) {
13
14
  directory = join(directory, segment);
14
- await mkdir(directory, { mode: 0o755 }).catch(error => { if (error.code !== 'EEXIST')
15
- throw error; });
16
- const stat = await lstat(directory);
17
- if (!stat.isDirectory() || stat.isSymbolicLink())
15
+ // Reject a redirected archive directory before creating or sharing anything below it.
16
+ const existing = await lstat(directory).catch((error) => {
17
+ if (error.code === 'ENOENT')
18
+ return undefined;
19
+ throw error;
20
+ });
21
+ if (existing && (!existing.isDirectory() || existing.isSymbolicLink()))
18
22
  throw new Error('workspace_worktree_skill_archive_unavailable');
19
23
  }
24
+ await ensureSharedDirectory(root, join(root, '.playcraft/skill-artifacts'));
20
25
  }
21
26
  /** Clone the saved commit and its exact skill archives, without copying unsaved files or credentials. */
22
27
  export async function checkoutChatWorktree(source, destination, commit, stateDir) {
@@ -60,7 +65,7 @@ export async function checkoutChatWorktree(source, destination, commit, stateDir
60
65
  if (createHash('sha256').update(archive).digest('hex') !== entry.revision.artifactHash)
61
66
  throw new Error('workspace_worktree_skill_archive_unavailable');
62
67
  await archiveDirectory(destination);
63
- await writeFile(saved, archive, { flag: 'wx', mode: 0o644 });
68
+ await writeSharedFile(destination, saved, archive);
64
69
  }
65
70
  // Existing locked repository bytes and the exact runtime cache can fill missing archives.
66
71
  await readWorkspaceSkills(destination, join(stateDir, 'skill-artifacts'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.57-beta",
3
+ "version": "0.1.58-beta",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "dev": "tsc -w",
26
- "build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-preview-runtime.mjs && tsc",
26
+ "build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-parameter-skill.mjs && node scripts/generate-preview-runtime.mjs && tsc",
27
27
  "start": "node dist/index.js",
28
28
  "test": "pnpm build && vitest run",
29
29
  "test:agent": "node scripts/run-agent-tests.mjs --list",
@@ -41,6 +41,7 @@
41
41
  "verify:platform-skills": "node scripts/generate-platform-skills.mjs --check",
42
42
  "generate:video-skills": "node scripts/generate-video-skills.mjs",
43
43
  "verify:video-skills": "node scripts/generate-video-skills.mjs --check && node --test scripts/generate-video-skills.test.mjs",
44
+ "verify:parameter-skill": "node scripts/generate-parameter-skill.mjs --check && node --test scripts/generate-parameter-skill.test.mjs",
44
45
  "verify:asset-skills": "node scripts/generate-asset-skills.mjs --check && node --test scripts/generate-asset-skills.test.mjs",
45
46
  "verify:agent-stop": "node scripts/verify-agent-stop.mjs",
46
47
  "verify-sprite-overlay": "node scripts/verify-sprite-overlay.mjs",
@@ -52,8 +53,8 @@
52
53
  "@gltf-transform/core": "^4.3.0",
53
54
  "@gltf-transform/extensions": "^4.3.0",
54
55
  "@gltf-transform/functions": "^4.3.0",
55
- "@playcraft/build": "0.1.57-beta",
56
- "@playcraft/common": "0.1.57-beta",
56
+ "@playcraft/build": "0.1.58-beta",
57
+ "@playcraft/common": "0.1.58-beta",
57
58
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
58
59
  "chokidar": "^4.0.3",
59
60
  "commander": "^13.1.0",
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: playcraft-runtime-parameters
3
+ description: Initialize or extend configurable parameters in a Cloud Workspace game by extracting real controls, connecting production consumers and a separate development preview, and generating the platform parameter panel from the project declarations.
4
+ compatibility: agent,opencode
5
+ version: 1.0.0
6
+ surfaces:
7
+ - sandbox-agent
8
+ metadata:
9
+ protocol: platform-protocol
10
+ ---
11
+
12
+ # Initialize game parameters
13
+
14
+ Turn the current game into a configurable engineering project. The platform renders its panel from the actual declaration; do not develop a game-specific platform panel. Work in the authorized engineering directory using the current execution mode and permissions.
15
+
16
+ Read [the parameter protocol](references/platform-protocol.md) before changing configuration, then [development preview integration](references/preview-integration.md) before connecting the game. The runtime supplies the task identity, report path and report structure separately; use those exact values.
17
+
18
+ ## Extract and connect
19
+
20
+ Inspect the production entry, engine, configuration and real consumers. Preserve existing paths, settings, scripts and gameplay. Reuse existing equivalent parameters before extracting constants. Select a bounded first group of useful controls whose effects can be tested; a game with different behavior should produce different controls. Do not copy a prior game's catalog or expose every constant.
21
+
22
+ Create or extend the schema, sparse theme overrides and platform labels. Connect a neutral resolver shared by production and preview to real consumers. A field declaration or unused getter alone is incomplete. Keep current visuals and behavior as defaults. Preserve user overrides, translated text and selected resources on repeated initialization.
23
+
24
+ For each consumed field, select an honest application mode: `live` for an in-place change, `restart-scene` for a change requiring a new game or scene, or `reload` when a new build is necessary. Register only real consumers. At least one meaningful live consumer should be connected when the game supports it; do not mark every field live to make initialization appear successful. Reuse the existing game's start/reset/dispose lifecycle for a restart.
25
+
26
+ ## Validate the generated project
27
+
28
+ Add or extend `test:parameters` with executable checks for defaults, sparse overrides and changed real-consumer behavior. Keep or restore a successful production `build`; retain an existing `typecheck`. Use the separate preview entry and production release boundary described in the integration reference.
29
+
30
+ The platform runs its own checks, builds the first development preview and derives capabilities from the real handshake. Report concrete incomplete work when the project cannot be adapted; do not fabricate receipts, successful builds or live capability. Preserve reviewable partial changes on failure.
31
+
32
+ Initialization may need one preview build because it changes source. Subsequent ordinary value edits must use the connected runtime and existing persistence, without calling the Agent or rebuilding. Keep development runtime modules out of production output under `dist/`.
@@ -0,0 +1,21 @@
1
+ # Existing parameter protocol
2
+
3
+ This is the Cloud Workspace-compatible interpretation of the supplied `platform-protocol` skill. Reuse its schema, `xplatform`, platform translations and sparse theme data; do not introduce a parallel `xplatform.parameter` declaration.
4
+
5
+ ## Declaration and defaults
6
+
7
+ `.playcraft/workspace.json` may declare `parameters: {schemaPath, dataPath, themeId}`. These are engineering-relative paths and the selected theme identity. Preserve unrelated manifest keys. Prefer an existing declaration and theme layout. For a new layout, use `gamePlay.preloaded`, `gamePlay.sections` and `gamePlay.settlement` for the three game-screen areas; organize their children around the current game rather than a sample catalog.
8
+
9
+ Object nodes use `type: "object"` and `properties`. Leaves use supported primitive types, valid defaults, meaningful units and known bounds or enums. Basic editable leaves support string, finite number, integer, boolean and null. Preserve existing standard `required` arrays. Unsupported references and conditional composition are not a reason to flatten complex data or silently remove constraints.
10
+
11
+ Missing theme values inherit supported schema defaults. A data file containing `{}` is valid. Editing creates a raw override; restoring a default removes that override. Never write every resolved value into the theme data. Preserve comments, unknown data, unrelated overrides and empty parent objects already present in the project.
12
+
13
+ Use `i18n.platform.json5` beside the schema with the same tree. `_self.title` and `_self.description` contain `zh` and `en` labels for the panel. Preserve `xplatform.ext.readonly`, ancestor read-only rules and `xplatform.showIf` / `showIfAny`. Visibility controls presentation rather than write permission.
14
+
15
+ ## Specialized values
16
+
17
+ Image, video and other resource leaves retain `xplatform.value_type: "path"` plus `mime_type`. Preserve their actual theme/shared path resolution and production asset registration. The platform already provides local upload, shared asset library, intelligent production and engineering-file selection; generated adapters consume the selected logical path rather than adding another asset-upload implementation.
18
+
19
+ Game text retains `xplatform.value_type: "i18n"` and an `i18n.runtime.<key>` reference. Keep global `i18n.runtime.json5` and per-theme `theme.i18n.runtime.json5` semantics and export the text from the real theme entry used in production. Supported languages are `en`, `zh`, `zh-TW`, `de`, `fr`, `es`, `ja`, `ko`, `pt`, `it`, `hi`, `bn`, `ur`, `ar`, `tr`, `ru`, `ms`, `id`. Keep complete language maps for overridden keys and remove an override only when its full value matches the inherited default. Explicit blank strings remain blank rather than falling back to another language.
20
+
21
+ Arrays, complex structures and unsupported constraints keep their original meaning. Do not disguise them as ordinary primitive controls to obtain a writable panel. An initialization can leave such controls explicitly unsupported while connecting a useful supported set.
@@ -0,0 +1,57 @@
1
+ # Development preview integration
2
+
3
+ The development SDK (Software Development Kit, 软件开发工具包) is separate from production advertising integration. Obtain it in the current engineering directory with:
4
+
5
+ ```sh
6
+ playcraft preview-runtime init --dir .
7
+ ```
8
+
9
+ It copies versioned modules, types and helpers to `.playcraft/preview-runtime` and refuses to overwrite modified managed files. Read its `README.md`, which documents the actual supported interfaces. Use a separate preview entry; keep the original production entry and advertising language/configuration behavior.
10
+
11
+ ## Actual game lifecycle
12
+
13
+ Import `connectPreviewRuntime` and `readWorkspacePreviewContext` from the copied `runtime.mjs` using the correct relative path. Read the real configuration contract version and pass the returned context to `connectPreviewRuntime` along with `fields`, `source`, `resolve` and `createGame`.
14
+
15
+ - `source.get/install` reads and replaces the actual configuration store; `resolve(documents)` shares production default/override rules.
16
+ - Each field registers its actual `documentId`, raw `path`, resolved `effectivePath`, `groupId` and application `mode`.
17
+ - `createGame(context)` returns real `apply`, `restartScene`, `afterRender` and `dispose` behavior. Honor `context.signal` and `context.assertCurrent()` before asynchronous visible changes. `afterRender` waits for a real frame.
18
+ - Live changes preserve progress and object identity where appropriate. Scene restart uses the game's own lifecycle and disposes obsolete objects, timers, event handlers and owned resources.
19
+ - Image consumers use `resource: "image"`, logical paths and `context.loadImage`. Prepare before installation, retain the previous image on a recoverable load failure, and honor explicit same-path refreshes. See the copied guide for resource error and lifecycle behavior.
20
+
21
+ Do not route development updates through an advertising configuration cache. Real field registration and a version-matched handshake establish capability; an integration report cannot do so.
22
+
23
+ ## Separate build
24
+
25
+ The project needs a compatible development Vite. Add `build:cloud-preview` invoking `node .playcraft/preview-runtime/build-preview.mjs --entry <actual-preview-entry>` with optional `--config <actual-preview-configuration>` and `--mount-id <actual-container>`. The helper does not implicitly load production Vite configuration. Adapt aliases, data imports, constants and asset handling explicitly when needed; preserve the production build system.
26
+
27
+ Preserve unrelated workspace manifest fields and add:
28
+
29
+ ```json
30
+ {
31
+ "previewRuntime": {
32
+ "version": 1,
33
+ "buildScript": "build:cloud-preview",
34
+ "outputPath": ".playcraft-editor-preview/index.html",
35
+ "reportPath": ".playcraft-editor-preview/report.json",
36
+ "configContractVersion": "actual-contract-version",
37
+ "entryPath": "src/preview/runtime.ts",
38
+ "gameFilePaths": [
39
+ "src/preview/runtime.ts",
40
+ "src/game/configuration.ts",
41
+ "src/game/game.ts",
42
+ ".playcraft/preview-runtime/runtime.mjs",
43
+ ".playcraft/preview-runtime/build-preview.mjs"
44
+ ]
45
+ }
46
+ }
47
+ ```
48
+
49
+ Replace example paths and contract version with actual existing inputs. Add optional `viteConfigPath` and `mountId` when used, and include all extra configuration inputs in `gameFilePaths`. The platform invokes its trusted packaged helper and supplies `PLAYCRAFT_RUNTIME_PREVIEW_CONTEXT`; manually inventing this context or authoring a successful module report is invalid.
50
+
51
+ ## Production boundary
52
+
53
+ Production must read the same neutral resolver and saved data while excluding the preview entry and runtime. With Vite, register `previewRuntimeReleaseGuard({root: process.cwd(), previewEntries: [...]})` from the copied `release-guard.mjs` in the actual production plugins, listing the real preview entry. Keep its default report path `.playcraft-editor-preview/release-report.json`; do not override `reportPath`. Configure production output at the engineering root `dist/`.
54
+
55
+ Initialization admission removes the previous report before the independent production build, then verifies the new report, actual module boundaries and the complete `dist/` artifact hashes. Missing or stale evidence, soft links, changed artifacts or development runtime markers leave initialization unverified. This establishes the stated module/output boundary, not a general audit of every possible plugin. Other builders need equivalent platform-supported evidence; otherwise report the unsupported boundary.
56
+
57
+ Keep managed runtime files out of parameter report `sources`, `origins` and `consumers`: they are recorded separately by the preview module report. Parameter evidence names the actual game consumer and resolver, not the copied development library. A successful parameter check or production build alone does not prove live behavior or production isolation.