@playcraft/cli 0.1.56-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 (101) hide show
  1. package/dist/assets/client.js +6 -4
  2. package/dist/commands/tools-3d.js +15 -0
  3. package/dist/commands/tools-generation.js +3 -2
  4. package/dist/commands/workspace.js +43 -0
  5. package/dist/utils/agent-api-client.js +31 -8
  6. package/dist/utils/agent-generation-provenance.js +45 -0
  7. package/dist/utils/generation-workspace-scope.js +50 -0
  8. package/dist/utils/image-generation-retry.js +7 -0
  9. package/dist/utils/tool-operation-journal.js +34 -4
  10. package/dist/workspace/messages.js +10 -0
  11. package/dist/workspace-runtime/batch/deliver.js +140 -80
  12. package/dist/workspace-runtime/batch/freeze.js +18 -6
  13. package/dist/workspace-runtime/batch/generate.js +68 -64
  14. package/dist/workspace-runtime/batch/prepare.js +109 -137
  15. package/dist/workspace-runtime/batch/records.js +4 -0
  16. package/dist/workspace-runtime/batch/routes.js +10 -2
  17. package/dist/workspace-runtime/batch/source-candidate.js +2 -0
  18. package/dist/workspace-runtime/batch/source-capture.js +1 -0
  19. package/dist/workspace-runtime/batch/source-failure.js +2 -0
  20. package/dist/workspace-runtime/batch/source-publish.js +3 -0
  21. package/dist/workspace-runtime/batch/writer-proof.js +37 -0
  22. package/dist/workspace-runtime/codex/app-server.js +1 -0
  23. package/dist/workspace-runtime/codex/config-toml.js +4 -0
  24. package/dist/workspace-runtime/codex/delivery-tools.js +7 -5
  25. package/dist/workspace-runtime/codex/generation-provenance-broker.js +80 -0
  26. package/dist/workspace-runtime/codex/launcher-client.js +18 -0
  27. package/dist/workspace-runtime/codex/model-broker.js +23 -4
  28. package/dist/workspace-runtime/persistence/journal.js +34 -0
  29. package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
  30. package/dist/workspace-runtime/previews/output.js +2 -0
  31. package/dist/workspace-runtime/previews/publish.js +20 -5
  32. package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
  33. package/dist/workspace-runtime/processes/managed-writes.js +47 -5
  34. package/dist/workspace-runtime/serve.js +19 -32
  35. package/dist/workspace-runtime/server/accept-operation.js +22 -2
  36. package/dist/workspace-runtime/server/directory-access.js +414 -0
  37. package/dist/workspace-runtime/server/dispatch.js +61 -23
  38. package/dist/workspace-runtime/server/endpoints.js +1 -0
  39. package/dist/workspace-runtime/server/http.js +52 -10
  40. package/dist/workspace-runtime/server/operation-record.js +2 -0
  41. package/dist/workspace-runtime/server/terminal-host.js +43 -8
  42. package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
  43. package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
  44. package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
  45. package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
  46. package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
  47. package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
  48. package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
  49. package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
  50. package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
  51. package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
  52. package/dist/workspace-runtime/workspaces/attachments.js +9 -0
  53. package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
  54. package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
  55. package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
  56. package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
  57. package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
  58. package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
  59. package/dist/workspace-runtime/workspaces/dependencies.js +105 -71
  60. package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
  61. package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
  62. package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
  63. package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
  64. package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
  65. package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
  66. package/dist/workspace-runtime/workspaces/files.js +4 -0
  67. package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
  68. package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
  69. package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
  70. package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
  71. package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
  72. package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
  73. package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
  74. package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
  75. package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
  76. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
  77. package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
  78. package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
  79. package/dist/workspace-runtime/workspaces/parameters.js +95 -3
  80. package/dist/workspace-runtime/workspaces/platform-export.js +190 -0
  81. package/dist/workspace-runtime/workspaces/prepare.js +4 -0
  82. package/dist/workspace-runtime/workspaces/registry.js +11 -0
  83. package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
  84. package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
  85. package/dist/workspace-runtime/workspaces/skills.js +14 -15
  86. package/dist/workspace-runtime/workspaces/transfer.js +4 -1
  87. package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
  88. package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
  89. package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
  90. package/package.json +5 -4
  91. package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
  92. package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
  93. package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
  94. package/project-template-v2/.playcraft/skills.lock.json +15 -8
  95. package/templates/preview-runtime/v1/README.md +4 -0
  96. package/templates/preview-runtime/v1/manifest.json +6 -6
  97. package/templates/preview-runtime/v1/runtime.mjs +368 -22
  98. package/templates/preview-runtime/v1/types/adapters.d.ts +4 -1
  99. package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
  100. package/templates/preview-runtime/v1/types/protocol.d.ts +19 -0
  101. package/templates/preview-runtime/v1/types/runtime.d.ts +2 -1
@@ -1,5 +1,5 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
1
+ import { createHash } from 'node:crypto';
2
+ import { cp, lstat, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
3
  import { basename, dirname, join } from 'node:path';
4
4
  import { load } from 'js-yaml';
5
5
  import { canonicalProjectSkillsJson, createCanonicalBundleArchive, effectiveProjectSkills, extractBundleArchive, inspectProjectSkills, readProjectSkillSource, readProjectSkillsLockFile, sourceTreeDigest, validateProjectSkillsSnapshot, } from '@playcraft/common/project-skills';
@@ -9,6 +9,7 @@ import { selectPlatformSkillSnapshot } from '../../project-skills/builtin-import
9
9
  import { EMPTY_WORKSPACE_REPOSITORY_SKILLS, EMPTY_WORKSPACE_PLATFORM_SKILLS } from './empty-template.js';
10
10
  import { excludeFromGit } from './dependencies.js';
11
11
  import { selectOptionalPlatformSkills } from './optional-platform-skills.js';
12
+ import { ensureSharedDirectory, writeSharedFile } from './shared-write.js';
12
13
  /** Runtime availability is determined by the locked metadata, independent of unsynced source edits. */
13
14
  export async function readWorkspaceSkillSnapshot(root) {
14
15
  const management = await lstat(join(root, '.playcraft'));
@@ -42,7 +43,7 @@ export async function ensureWorkspaceSkills(root, cache, access, options = {}) {
42
43
  continue;
43
44
  await safeDirectory(root, '.agents/skills');
44
45
  const source = `.agents/skills/${name}`;
45
- await mkdir(join(root, source), { mode: 0o755 });
46
+ await ensureSharedDirectory(root, join(root, source));
46
47
  added.add(source);
47
48
  const template = join(resolveCliPackageRoot(), 'project-template/.claude/skills', name);
48
49
  for (const child of await readdir(template))
@@ -61,12 +62,16 @@ async function safeDirectory(root, path) {
61
62
  let current = root;
62
63
  for (const segment of path.split('/')) {
63
64
  current = join(current, segment);
64
- await mkdir(current, { mode: 0o755 }).catch(error => { if (error.code !== 'EEXIST')
65
- throw error; });
66
- const stat = await lstat(current);
67
- if (!stat.isDirectory() || stat.isSymbolicLink())
65
+ // Validate every existing segment before creating anything, so a link never redirects the tree.
66
+ const stat = await lstat(current).catch((error) => {
67
+ if (error.code === 'ENOENT')
68
+ return undefined;
69
+ throw error;
70
+ });
71
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
68
72
  throw new Error('workspace_skills_invalid');
69
73
  }
74
+ await ensureSharedDirectory(root, current);
70
75
  return current;
71
76
  }
72
77
  /** Cache misses may reconstruct only the exact locked repository bytes, never a newer source. */
@@ -111,14 +116,7 @@ async function lockedFiles(root, cache, entry, view, loadArtifact) {
111
116
  throw new Error('workspace_skills_invalid_bundle');
112
117
  if (entry.revision.kind === 'source' && sourceTreeDigest(extracted.files.map(file => ({ path: file.path, content: file.data }))) !== entry.sourceTreeHash)
113
118
  throw new Error('workspace_skills_digest_mismatch');
114
- const temporary = `${saved}.${randomUUID()}.tmp`;
115
- try {
116
- await writeFile(temporary, archive, { flag: 'wx', mode: 0o644 });
117
- await rename(temporary, saved);
118
- }
119
- finally {
120
- await rm(temporary, { force: true });
121
- }
119
+ await writeSharedFile(root, saved, archive);
122
120
  return extracted.files;
123
121
  }
124
122
  function metadata(entryKey, content) {
@@ -155,6 +153,7 @@ export async function freezeWorkspaceSkills(root, cache, loadArtifact) {
155
153
  const relative = join(entry.entryKey, file.path);
156
154
  expectedFiles.add(relative);
157
155
  await safeDirectory(generation, dirname(relative));
156
+ // shared
158
157
  await writeFile(target, file.data, { flag: 'wx', mode: 0o444 }).catch(async (error) => {
159
158
  if (error.code !== 'EEXIST')
160
159
  throw error;
@@ -138,7 +138,9 @@ async function copySnapshot(source, target, signal, hashOnly = false) {
138
138
  size += chunk.length;
139
139
  callback(size > before.size ? new WorkspaceTransferError('workspace_transfer_conflict') : null, chunk);
140
140
  } });
141
- await pipeline(handle.createReadStream({ autoClose: false }), guard, createWriteStream(to, { flags: 'wx', mode: 0o600 }), { signal });
141
+ await pipeline(handle.createReadStream({ autoClose: false }), guard,
142
+ // private
143
+ createWriteStream(to, { flags: 'wx', mode: 0o600 }), { signal });
142
144
  }
143
145
  else {
144
146
  for await (const chunk of handle.createReadStream({ autoClose: false })) {
@@ -366,6 +368,7 @@ export async function applyWorkspaceTransfer(root, bundle, expectedTree, options
366
368
  try {
367
369
  const bundlePath = typeof bundle === 'string' ? resolve(bundle) : join(temporary, 'incoming.bundle');
368
370
  const incoming = join(temporary, 'checkout');
371
+ // private
369
372
  if (typeof bundle !== 'string')
370
373
  await writeFile(bundlePath, bundle, { mode: 0o600 });
371
374
  const bundleInfo = await stat(bundlePath);
@@ -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.56-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.56-beta",
56
- "@playcraft/common": "0.1.56-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.
@@ -10,8 +10,8 @@
10
10
  },
11
11
  {
12
12
  "skillRef": "platform/playcraft-asset-management",
13
- "version": "1.0.0",
14
- "artifactHash": "27209dba24f6e1851dc83682fbd24165894c802794c797cac32d8871f740cf14",
13
+ "version": "1.1.0",
14
+ "artifactHash": "0b5408792edfeb7b852817bd76893b95738a0e161407f0b48c4b50161797ba3c",
15
15
  "surface": "portal-agent",
16
16
  "mountPath": ".playcraft/skills/platform__playcraft-asset-management"
17
17
  },
@@ -108,8 +108,8 @@
108
108
  },
109
109
  {
110
110
  "skillRef": "remix/playcraft-audio-generation",
111
- "version": "1.0.0",
112
- "artifactHash": "8d1b035b289545ca8fd6c247b9417bf34bddf5a26b31e114c35d001aa3a9ebf0",
111
+ "version": "1.1.1",
112
+ "artifactHash": "27661b1b52e7981a0b9cf5531f535fcf10928ea290348fcb469e35ad2082b437",
113
113
  "surface": "sandbox-agent",
114
114
  "mountPath": ".playcraft/skills/remix__playcraft-audio-generation"
115
115
  },
@@ -122,15 +122,15 @@
122
122
  },
123
123
  {
124
124
  "skillRef": "remix/playcraft-image-generation",
125
- "version": "1.0.0",
126
- "artifactHash": "91bec2422c750d9d47d5be2796ec5aaffb48de561f2c2d8b92112f37d576f489",
125
+ "version": "1.1.2",
126
+ "artifactHash": "a006c96700f8dbfe4c34a31d0e1646620ca6148d319e59e7466d627fea1ca65e",
127
127
  "surface": "sandbox-agent",
128
128
  "mountPath": ".playcraft/skills/remix__playcraft-image-generation"
129
129
  },
130
130
  {
131
131
  "skillRef": "remix/playcraft-image-processing",
132
- "version": "1.0.0",
133
- "artifactHash": "d845c067bed103c3d1d3f145b1cb8246ffbd6a9ae91efcfbce2eae456819212d",
132
+ "version": "1.1.1",
133
+ "artifactHash": "903f4efc14e2bb57403856edeca43146904ec64811cfd107b6ec76bb451842fd",
134
134
  "surface": "sandbox-agent",
135
135
  "mountPath": ".playcraft/skills/remix__playcraft-image-processing"
136
136
  },
@@ -147,6 +147,13 @@
147
147
  "artifactHash": "e2a59c61ef5b094a821fbbab3b0f65f906bfc0dfda6f3d8c0b33a6f460dd0963",
148
148
  "surface": "sandbox-agent",
149
149
  "mountPath": ".playcraft/skills/remix__playcraft-remix-workflow"
150
+ },
151
+ {
152
+ "skillRef": "remix/playcraft-workspace-assets",
153
+ "version": "1.0.0",
154
+ "artifactHash": "f098949b043a229e561d92a34c3a65b44ffbd83d1758ef30ac09d2d0d5ea2570",
155
+ "surface": "sandbox-agent",
156
+ "mountPath": ".playcraft/skills/remix__playcraft-workspace-assets"
150
157
  }
151
158
  ]
152
159
  }
@@ -22,6 +22,10 @@ Prepare all required images before changing visible objects, call `assertCurrent
22
22
 
23
23
  `ResourceLoadError` is exported from `runtime.mjs`. Live adapters let this error reach the runtime, which retains the last known installation and reports a recoverable, field-level resource failure. Do not catch an arbitrary update exception and claim successful fallback. During initial `createGame`, catch only this load error to start with an embedded default image or empty background; the runtime still reports the saved image as unapplied. A later patch of that field retries it; unrelated parameter updates can continue. `resourceErrors` and file-save receipts are independent. Save logical paths and let the original production asset registry include the file when building delivery output.
24
24
 
25
+ The endpoint advertises `canRefreshImages: true` for explicit same-path content refresh. The host sends registered live-image fields and their expected file digests through a separate resource sequence; raw parameters and draft revisions remain unchanged. In `apply`, honor `context.fields` even when a logical path equals its previous value: request that field through `loadImage` and install the returned image after `assertCurrent()`. A local path-equality cache must not hide this explicit update. The runtime prepares new images without blocking unrelated live parameters, checks the expected content digest, rebases against the latest permitted configuration and records installed digests only after the real render acknowledgement. A matching installed digest is a no-op; a failed or superseded preparation leaves the old image available and can be retried. Older endpoints without this optional capability require the existing rebuild route.
26
+
27
+ Content generations are independent per field. If an opening image and board image are being prepared together, a newer board-only refresh cancels only the old board read; the still-current opening image must finish and apply while preserving the newer board. Each application uses only the prepared fields that remain current and the latest permitted configuration. A preparation failure in a still-current batch retains the old images and marks all its unapplied fields as recoverable, so an explicit retry cannot silently forget a prepared sibling.
28
+
25
29
  ## Preview build
26
30
 
27
31
  The engineering project must install a compatible Vite as a development dependency. Set a separate package script:
@@ -4,14 +4,14 @@
4
4
  "protocolVersion": 1,
5
5
  "files": {
6
6
  "build-preview.mjs": "2bd512675d829cc07ceddcb58365a6f74d36ff93690f8d72f4bbaa1d45511c91",
7
- "README.md": "f104440b3acb23adb88bde0dff23840fa170a14f477d1d7338575eca75e73571",
7
+ "README.md": "3e35ca2ce6eab84ba8688b400aee8ee5e69458466a13f00e459fd630575d9cbd",
8
8
  "release-guard.mjs": "0c33d1559c68844f3760c3e754ee36ca70dc27151ef6f7ad62f29f95239679cc",
9
9
  "runtime.d.mts": "a366552166a4a93103306997058951416740eaf6d6f3d0a34854386b4f1e02f5",
10
10
  "runtime.d.ts": "a366552166a4a93103306997058951416740eaf6d6f3d0a34854386b4f1e02f5",
11
- "runtime.mjs": "25b33218ff32dfeaaad7bcd69647cbf662fa4312d49b62724a2104fd27d9b001",
12
- "types/adapters.d.ts": "29b5c18d4576ab00eca44875b5c4eae2fc0e0ed6ab5595dbbaf6ba9b9fd8beed",
13
- "types/preview.d.ts": "19ea55ed6faf4faa4f4fc62a2d70309d7ba917fc165c2c149856798e1b4197f8",
14
- "types/protocol.d.ts": "716e83802336c4e7377abc1b433c77045ed59997a45646fe2fdbe55cb82c5e62",
15
- "types/runtime.d.ts": "efcd429b24d75b06afb3f05ec3f8134efc381ede0dd59cb43ec5770e2559c77b"
11
+ "runtime.mjs": "700c6cb0859ef4b4390d7a1dae41f7e8fa7be7c8dad90b5b48a7e7ee5f90a6a1",
12
+ "types/adapters.d.ts": "f5e70c4bb9043de1c590eada14266f613286360c809faf26680b0bbbd1cc7b2c",
13
+ "types/preview.d.ts": "db2e7eb1fa3406ca53fc7c3a4352da061c05af714ead978111ab5118de58db32",
14
+ "types/protocol.d.ts": "b298d1d35d6e261bbf6a0500f9029f38cad1532bb46475566180b3b92833c507",
15
+ "types/runtime.d.ts": "f52d08d7b53ff84394381f9d1741a624142f75fac468abf9a5029e98fe172ca4"
16
16
  }
17
17
  }