@playcraft/cli 0.1.60-beta → 0.1.62-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 (40) hide show
  1. package/dist/commands/build.js +16 -7
  2. package/dist/project-skills/builtin-import.js +2 -2
  3. package/dist/skill-registry/messages.js +2 -2
  4. package/dist/skill-registry/platform-package.js +2 -4
  5. package/dist/utils/agent-api-client.js +22 -0
  6. package/dist/workspace-runtime/codex/app-server.js +24 -1
  7. package/dist/workspace-runtime/codex/config-toml.js +9 -5
  8. package/dist/workspace-runtime/codex/delivery-tools.js +3 -10
  9. package/dist/workspace-runtime/codex/launcher-client.js +25 -13
  10. package/dist/workspace-runtime/codex/model-broker.js +13 -3
  11. package/dist/workspace-runtime/codex/production-skill-tools.js +95 -0
  12. package/dist/workspace-runtime/processes/skill-command.js +130 -0
  13. package/dist/workspace-runtime/serve.js +12 -1
  14. package/dist/workspace-runtime/server/accept-operation.js +10 -1
  15. package/dist/workspace-runtime/server/directory-access.js +11 -4
  16. package/dist/workspace-runtime/server/dispatch.js +70 -3
  17. package/dist/workspace-runtime/server/http.js +61 -3
  18. package/dist/workspace-runtime/server/operation-record.js +4 -1
  19. package/dist/workspace-runtime/workspaces/dependencies.js +35 -10
  20. package/dist/workspace-runtime/workspaces/developer-command.js +65 -64
  21. package/dist/workspace-runtime/workspaces/directories.js +4 -1
  22. package/dist/workspace-runtime/workspaces/empty-template.js +3 -1
  23. package/dist/workspace-runtime/workspaces/platform-export.js +3 -3
  24. package/dist/workspace-runtime/workspaces/procedural-3d/candidate.js +129 -0
  25. package/dist/workspace-runtime/workspaces/procedural-3d/contracts.js +34 -0
  26. package/dist/workspace-runtime/workspaces/procedural-3d/files.js +83 -0
  27. package/dist/workspace-runtime/workspaces/procedural-3d/host.js +595 -0
  28. package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-build.js +184 -0
  29. package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-recovery.js +77 -0
  30. package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-store.js +170 -0
  31. package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-worker.js +85 -0
  32. package/dist/workspace-runtime/workspaces/procedural-3d/read-candidates.js +219 -0
  33. package/dist/workspace-runtime/workspaces/procedural-3d/render-worker.js +159 -0
  34. package/dist/workspace-runtime/workspaces/procedural-3d/review-contracts.js +257 -0
  35. package/dist/workspace-runtime/workspaces/procedural-3d/review-page.js +26 -0
  36. package/dist/workspace-runtime/workspaces/skill-execution-context.js +69 -0
  37. package/dist/workspace-runtime/workspaces/skills.js +15 -5
  38. package/package.json +4 -3
  39. package/project-template-v2/.playcraft/skills.lock.json +7 -0
  40. package/project-templates/empty/AGENTS.md +11 -1
@@ -84,8 +84,9 @@ async function request(state, path, init) {
84
84
  }
85
85
  /**
86
86
  * Saves the directory (commit + push), then exports that commit for the channels through the
87
- * platform, as the delivery panel's Export does. Each channel repeats its latest export of the
88
- * branch, or uses the defaults without one. A standalone workspace first becomes a project.
87
+ * platform, as the delivery panel's Export does. Each channel uses that panel's fixed HTML or
88
+ * ZIP choice. Other options repeat the channel's latest export of the branch, or stay empty
89
+ * without one. A standalone workspace first becomes a project.
89
90
  */
90
91
  export async function startPlatformExport(state, input) {
91
92
  await promoteStandalone(state, input);
@@ -121,7 +122,6 @@ export async function startPlatformExport(state, input) {
121
122
  branchName: status.branch,
122
123
  commitHash: commit,
123
124
  platforms: input.channels,
124
- ...(input.format ? { format: input.format } : {}),
125
125
  clientRequestId: input.clientRequestId,
126
126
  },
127
127
  });
@@ -0,0 +1,129 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstat, readdir, rename, rm } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { ensureSharedDirectory } from '../shared-write.js';
5
+ import { createHostReviewFiles } from './review-page.js';
6
+ import { REVIEW_DIRECTORY, REVIEW_PAGE, reviewProfileHash, validateRenderReceipt, } from './review-contracts.js';
7
+ import { checkedFile, copySnapshot, dependencyHash, digest, relativeFile, snapshotFiles, verifyFiles, } from './files.js';
8
+ export async function freezeCandidate(root, jobRoot, input, lockedHelpers) {
9
+ if ((!input.review && (!input.pagePath || !input.files.includes(input.pagePath))) ||
10
+ !input.sourcePaths.includes('src/createObjectModel.ts') ||
11
+ input.files.includes('object-sculpt-spec.json') ||
12
+ input.sourcePaths.some((path) => !input.files.includes(path)))
13
+ throw new Error('procedural_skill_missing_candidate_inputs');
14
+ const files = await snapshotFiles(join(jobRoot, 'workspace'), input.files);
15
+ if (input.review) {
16
+ if (!lockedHelpers ||
17
+ input.pagePath ||
18
+ !input.files.includes(input.review.factoryPath) ||
19
+ input.files.some((path) => path.toLowerCase().startsWith(`${REVIEW_DIRECTORY}/`)))
20
+ throw new Error('procedural_skill_invalid_review_input');
21
+ for (const file of createHostReviewFiles(input.review, lockedHelpers))
22
+ files.push({ ...file, hash: digest(file.bytes) });
23
+ files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
24
+ if (files.length > 256 ||
25
+ files.reduce((size, file) => size + file.bytes.length, 0) > 64 * 1024 * 1024)
26
+ throw new Error('procedural_skill_size_limit');
27
+ }
28
+ const id = randomUUID();
29
+ await copySnapshot(root, join(jobRoot, 'candidates', id, 'source'), files);
30
+ const spec = await checkedFile(join(jobRoot, 'workspace'), 'object-sculpt-spec.json');
31
+ await copySnapshot(root, join(jobRoot, 'candidates', id), [{ path: 'spec.json', bytes: spec }]);
32
+ const hashes = files.map(({ path, hash }) => ({ path, hash }));
33
+ return {
34
+ id,
35
+ files: hashes,
36
+ sourcePaths: [...new Set(input.sourcePaths)].sort(),
37
+ sourceHash: digest(JSON.stringify(hashes)),
38
+ dependencyHash: await dependencyHash(root),
39
+ pagePath: relativeFile(input.review ? REVIEW_PAGE : input.pagePath),
40
+ views: input.views,
41
+ specHash: digest(spec),
42
+ expectedSpecHash: digest(spec),
43
+ passId: input.passId,
44
+ ...(input.review
45
+ ? {
46
+ hostReview: {
47
+ profile: input.review.profile,
48
+ profileHash: reviewProfileHash(input.review.profile),
49
+ helperHash: digest(lockedHelpers.scene),
50
+ profileHelperHash: digest(lockedHelpers.profile),
51
+ },
52
+ }
53
+ : {}),
54
+ };
55
+ }
56
+ export const candidateWorkspaceFiles = (candidate) => candidate.hostReview
57
+ ? candidate.files.filter((file) => !file.path.startsWith(`${REVIEW_DIRECTORY}/`))
58
+ : candidate.files;
59
+ export async function verifyCandidate(root, jobRoot, candidate) {
60
+ if ((await dependencyHash(root)) !== candidate.dependencyHash)
61
+ throw new Error('procedural_skill_dependencies_changed');
62
+ await verifyFiles(join(jobRoot, 'workspace'), candidateWorkspaceFiles(candidate));
63
+ await verifyFiles(join(jobRoot, 'candidates', candidate.id, 'source'), candidate.files);
64
+ if (digest(await checkedFile(join(jobRoot, 'workspace'), 'object-sculpt-spec.json')) !==
65
+ candidate.expectedSpecHash ||
66
+ digest(await checkedFile(join(jobRoot, 'candidates', candidate.id), 'spec.json')) !==
67
+ candidate.specHash)
68
+ throw new Error('procedural_skill_spec_changed');
69
+ if (!candidate.captures?.length)
70
+ throw new Error('procedural_skill_captures_missing');
71
+ await verifyFiles(jobRoot, candidate.captures);
72
+ if (candidate.hostReview) {
73
+ if (!candidate.renderReceipt)
74
+ throw new Error('procedural_skill_review_receipt_missing');
75
+ const bytes = await checkedFile(jobRoot, candidate.renderReceipt.path);
76
+ if (digest(bytes) !== candidate.renderReceipt.hash)
77
+ throw new Error('procedural_skill_review_receipt_changed');
78
+ validateRenderReceipt(JSON.parse(bytes.toString('utf8')), candidate.hostReview.profile, candidate.views);
79
+ }
80
+ }
81
+ /** Publish a new immutable source directory; never overwrite the agent's current factory. */
82
+ export async function adoptCandidate(root, jobRoot, candidate, assetId) {
83
+ await verifyCandidate(root, jobRoot, candidate);
84
+ if (candidate.review?.decision !== 'accept' ||
85
+ candidate.review.sourceHash !== candidate.sourceHash)
86
+ throw new Error('procedural_skill_review_required');
87
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(assetId))
88
+ throw new Error('procedural_skill_invalid_asset_id');
89
+ const relative = `src/generated/${assetId}/${candidate.id}`;
90
+ const target = join(root, relative);
91
+ const expected = candidate.files.filter((file) => candidate.sourcePaths.includes(file.path));
92
+ await ensureSharedDirectory(root, dirname(target));
93
+ const existing = await lstat(target).catch((error) => {
94
+ if (error.code === 'ENOENT')
95
+ return undefined;
96
+ throw error;
97
+ });
98
+ if (existing) {
99
+ if (!existing.isDirectory() || existing.isSymbolicLink())
100
+ throw new Error('procedural_skill_adoption_conflict');
101
+ const expectedPaths = new Set(expected.map((file) => file.path));
102
+ const checkTree = async (directory = '') => {
103
+ for (const item of await readdir(join(target, directory), { withFileTypes: true })) {
104
+ const path = directory ? `${directory}/${item.name}` : item.name;
105
+ if (item.isDirectory() && expected.some((file) => file.path.startsWith(`${path}/`)))
106
+ await checkTree(path);
107
+ else if (!item.isFile() || !expectedPaths.has(path))
108
+ throw new Error('procedural_skill_adoption_conflict');
109
+ }
110
+ };
111
+ await checkTree();
112
+ await verifyFiles(target, expected);
113
+ return { path: relative, sourceHash: candidate.sourceHash };
114
+ }
115
+ const staging = `${target}.${randomUUID()}.pending`;
116
+ try {
117
+ const files = await Promise.all(expected.map(async (file) => ({
118
+ path: file.path,
119
+ bytes: await checkedFile(join(jobRoot, 'candidates', candidate.id, 'source'), file.path),
120
+ })));
121
+ await copySnapshot(root, staging, files);
122
+ await verifyFiles(staging, expected);
123
+ await rename(staging, target);
124
+ return { path: relative, sourceHash: candidate.sourceHash };
125
+ }
126
+ finally {
127
+ await rm(staging, { recursive: true, force: true });
128
+ }
129
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export const requestSchema = z
3
+ .object({
4
+ action: z.enum([
5
+ 'describe',
6
+ 'preflight',
7
+ 'prepare',
8
+ 'status',
9
+ 'recover',
10
+ 'next',
11
+ 'run-step',
12
+ 'request-correction',
13
+ 'capture',
14
+ 'review',
15
+ 'adopt',
16
+ ]),
17
+ runId: z.string().uuid().optional(),
18
+ expectedRevision: z.number().int().nonnegative().optional(),
19
+ input: z.record(z.unknown()).default({}),
20
+ })
21
+ .strict();
22
+ export const captureSchema = z
23
+ .object({
24
+ pagePath: z.string(),
25
+ files: z.array(z.string()).min(2).max(256),
26
+ sourcePaths: z.array(z.string()).min(1).max(128),
27
+ views: z
28
+ .array(z.enum(['front', 'right', 'back', 'left', 'hero']))
29
+ .min(3)
30
+ .max(5)
31
+ .default(['front', 'right', 'hero']),
32
+ })
33
+ .strict()
34
+ .refine((input) => new Set(input.views).size === input.views.length && input.views.includes('hero'));
@@ -0,0 +1,83 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { ensureSharedDirectory, writeSharedFile } from '../shared-write.js';
5
+ export const digest = (data) => createHash('sha256').update(data).digest('hex');
6
+ export function relativeFile(value) {
7
+ if (!value ||
8
+ value.length > 512 ||
9
+ value.includes('\\') ||
10
+ value.includes('\0') ||
11
+ value.split('/').some((part) => !part || part === '.' || part === '..') ||
12
+ value.startsWith('/'))
13
+ throw new Error('procedural_skill_invalid_path');
14
+ return value;
15
+ }
16
+ export async function checkedFile(root, path, max = 16 * 1024 * 1024) {
17
+ const segments = relativeFile(path).split('/');
18
+ let current = resolve(root);
19
+ const base = await lstat(current);
20
+ if (!base.isDirectory() || base.isSymbolicLink())
21
+ throw new Error('procedural_skill_invalid_path');
22
+ for (let index = 0; index < segments.length; index++) {
23
+ current = join(current, segments[index]);
24
+ const stat = await lstat(current);
25
+ if (stat.isSymbolicLink() ||
26
+ (index < segments.length - 1
27
+ ? !stat.isDirectory()
28
+ : !stat.isFile() || stat.nlink !== 1 || stat.size > max))
29
+ throw new Error('procedural_skill_invalid_file');
30
+ }
31
+ return readFile(current);
32
+ }
33
+ export async function snapshotFiles(root, paths) {
34
+ if (!paths.length || paths.length > 256 || new Set(paths).size !== paths.length)
35
+ throw new Error('procedural_skill_invalid_files');
36
+ const files = [];
37
+ let size = 0;
38
+ for (const path of [...paths].sort()) {
39
+ const bytes = await checkedFile(root, path);
40
+ size += bytes.length;
41
+ if (size > 64 * 1024 * 1024)
42
+ throw new Error('procedural_skill_size_limit');
43
+ files.push({ path, bytes, hash: digest(bytes) });
44
+ }
45
+ return files;
46
+ }
47
+ export async function verifyFiles(root, files) {
48
+ for (const file of files)
49
+ if (digest(await checkedFile(root, file.path)) !== file.hash)
50
+ throw new Error('procedural_skill_candidate_changed');
51
+ }
52
+ export async function dependencyHash(root) {
53
+ const parts = [];
54
+ for (const name of ['package.json', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']) {
55
+ try {
56
+ parts.push(`${name}:${digest(await checkedFile(root, name, 16 * 1024 * 1024))}`);
57
+ }
58
+ catch (error) {
59
+ if (error.code !== 'ENOENT')
60
+ throw error;
61
+ }
62
+ }
63
+ if (!parts.some((part) => part.startsWith('package.json:')))
64
+ throw new Error('procedural_skill_project_dependencies_missing');
65
+ return digest(parts.join('\n'));
66
+ }
67
+ /** Control-owned provenance is separate from the agent-writable job workspace. */
68
+ export async function writeControlJson(path, value) {
69
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 }); // private
70
+ const temporary = `${path}.${randomUUID()}.tmp`;
71
+ try {
72
+ await writeFile(temporary, JSON.stringify(value), { flag: 'wx', mode: 0o600 }); // private
73
+ await rename(temporary, path);
74
+ }
75
+ finally {
76
+ await rm(temporary, { force: true });
77
+ }
78
+ }
79
+ export async function copySnapshot(root, target, files) {
80
+ await ensureSharedDirectory(root, target);
81
+ for (const file of files)
82
+ await writeSharedFile(root, join(target, relativeFile(file.path)), file.bytes);
83
+ }