@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.
- package/dist/commands/build.js +16 -7
- package/dist/project-skills/builtin-import.js +2 -2
- package/dist/skill-registry/messages.js +2 -2
- package/dist/skill-registry/platform-package.js +2 -4
- package/dist/utils/agent-api-client.js +22 -0
- package/dist/workspace-runtime/codex/app-server.js +24 -1
- package/dist/workspace-runtime/codex/config-toml.js +9 -5
- package/dist/workspace-runtime/codex/delivery-tools.js +3 -10
- package/dist/workspace-runtime/codex/launcher-client.js +25 -13
- package/dist/workspace-runtime/codex/model-broker.js +13 -3
- package/dist/workspace-runtime/codex/production-skill-tools.js +95 -0
- package/dist/workspace-runtime/processes/skill-command.js +130 -0
- package/dist/workspace-runtime/serve.js +12 -1
- package/dist/workspace-runtime/server/accept-operation.js +10 -1
- package/dist/workspace-runtime/server/directory-access.js +11 -4
- package/dist/workspace-runtime/server/dispatch.js +70 -3
- package/dist/workspace-runtime/server/http.js +61 -3
- package/dist/workspace-runtime/server/operation-record.js +4 -1
- package/dist/workspace-runtime/workspaces/dependencies.js +35 -10
- package/dist/workspace-runtime/workspaces/developer-command.js +65 -64
- package/dist/workspace-runtime/workspaces/directories.js +4 -1
- package/dist/workspace-runtime/workspaces/empty-template.js +3 -1
- package/dist/workspace-runtime/workspaces/platform-export.js +3 -3
- package/dist/workspace-runtime/workspaces/procedural-3d/candidate.js +129 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/contracts.js +34 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/files.js +83 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/host.js +595 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-build.js +184 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-recovery.js +77 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-store.js +170 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/model-preview-worker.js +85 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/read-candidates.js +219 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/render-worker.js +159 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/review-contracts.js +257 -0
- package/dist/workspace-runtime/workspaces/procedural-3d/review-page.js +26 -0
- package/dist/workspace-runtime/workspaces/skill-execution-context.js +69 -0
- package/dist/workspace-runtime/workspaces/skills.js +15 -5
- package/package.json +4 -3
- package/project-template-v2/.playcraft/skills.lock.json +7 -0
- 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
|
|
88
|
-
*
|
|
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
|
+
}
|