@playcraft/cli 0.1.71 → 0.1.72
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/cli-root-help.js +1 -0
- package/dist/commands/references.js +3 -0
- package/dist/commands/tools-batch.js +2 -0
- package/dist/references/messages.js +2 -0
- package/dist/references/protocol.js +39 -12
- package/dist/workspace-runtime/batch/freeze.js +94 -24
- package/dist/workspace-runtime/batch/records.js +12 -6
- package/dist/workspace-runtime/batch/routes.js +31 -11
- package/dist/workspace-runtime/batch/source-candidate.js +29 -16
- package/dist/workspace-runtime/batch/source-capture.js +251 -20
- package/dist/workspace-runtime/codex/app-server.js +7 -2
- package/dist/workspace-runtime/codex/remix-references-broker.js +208 -67
- package/dist/workspace-runtime/server/directory-access.js +10 -0
- package/dist/workspace-runtime/server/http.js +332 -118
- package/dist/workspace-runtime/workspaces/composer-context.js +92 -41
- package/dist/workspace-runtime/workspaces/files.js +39 -9
- package/dist/workspace-runtime/workspaces/remix-references.js +162 -53
- package/dist/workspace-runtime/workspaces/starter-template.js +74 -0
- package/package.json +4 -3
|
@@ -4,20 +4,47 @@ import { lstat, mkdir, open, readFile, realpath, rename, rm, writeFile } from 'n
|
|
|
4
4
|
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
5
5
|
import { gzip, gunzip } from 'node:zlib';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
|
-
import { WORKSPACE_REMIX_REFERENCES_MANIFEST } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
|
+
import { WORKSPACE_REMIX_REFERENCES_MANIFEST, starterTemplateTreeHashInput, remixReferenceContentHashInput, workspaceStarterTemplateSnapshotSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
8
8
|
import { excludeFromGit } from '../workspaces/dependencies.js';
|
|
9
|
-
import { batchReferenceEnvExamples, batchReferenceMetadata } from '../workspaces/remix-references.js';
|
|
9
|
+
import { batchReferenceEnvExamples, batchReferenceMetadata, } from '../workspaces/remix-references.js';
|
|
10
10
|
import { gitRead, gitWrite } from './git.js';
|
|
11
11
|
const compress = promisify(gzip);
|
|
12
12
|
const decompress = promisify(gunzip);
|
|
13
|
-
const privateNames = new Set([
|
|
14
|
-
'.
|
|
15
|
-
'
|
|
13
|
+
const privateNames = new Set([
|
|
14
|
+
'.git',
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.ssh',
|
|
17
|
+
'.aws',
|
|
18
|
+
'.azure',
|
|
19
|
+
'.gnupg',
|
|
20
|
+
'.codex',
|
|
21
|
+
'.npm',
|
|
22
|
+
'.pnpm-store',
|
|
23
|
+
'.config',
|
|
24
|
+
'.npmrc',
|
|
25
|
+
'.yarnrc',
|
|
26
|
+
'.yarnrc.yml',
|
|
27
|
+
'.netrc',
|
|
28
|
+
'.git-credentials',
|
|
29
|
+
'credentials.json',
|
|
30
|
+
'credentials',
|
|
31
|
+
'secrets',
|
|
32
|
+
'id_rsa',
|
|
33
|
+
'id_ed25519',
|
|
34
|
+
'settings.local.json',
|
|
35
|
+
'.playcraft.json',
|
|
36
|
+
'playcraft.config.json',
|
|
37
|
+
'.playcraft-temp',
|
|
38
|
+
'.playcraft-preview-out',
|
|
39
|
+
]);
|
|
16
40
|
function included(path, referenceExamples) {
|
|
17
41
|
const parts = path.split('/');
|
|
18
|
-
return !parts.some((name, index) => privateNames.has(name) ||
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
return (!parts.some((name, index) => privateNames.has(name) ||
|
|
43
|
+
(/^\.env(?:$|\.)/.test(name) &&
|
|
44
|
+
!(index === parts.length - 1 && referenceExamples.has(path))) ||
|
|
45
|
+
/\.(?:pem|key|p12|pfx)$/.test(name)) &&
|
|
46
|
+
!path.startsWith('.playcraft/skills/') &&
|
|
47
|
+
!path.startsWith('.playcraft/skill-selections/'));
|
|
21
48
|
}
|
|
22
49
|
/** Git keeps only the executable bit, so the digest must not depend on umask or group-write bits. */
|
|
23
50
|
function snapshotMode(mode) {
|
|
@@ -30,7 +57,13 @@ function inside(root, path) {
|
|
|
30
57
|
export function digestFiles(files) {
|
|
31
58
|
const hash = createHash('sha256');
|
|
32
59
|
for (const [path, file] of files)
|
|
33
|
-
hash
|
|
60
|
+
hash
|
|
61
|
+
.update(path)
|
|
62
|
+
.update('\0')
|
|
63
|
+
.update(String(file.mode))
|
|
64
|
+
.update('\0')
|
|
65
|
+
.update(file.body)
|
|
66
|
+
.update('\0');
|
|
34
67
|
return hash.digest('hex');
|
|
35
68
|
}
|
|
36
69
|
/** Files Git will accept. Credentials, caches and node_modules are left out. */
|
|
@@ -38,13 +71,15 @@ export async function readSourceFiles(root) {
|
|
|
38
71
|
const source = await realpath(root);
|
|
39
72
|
const referenceExamples = await batchReferenceEnvExamples(source);
|
|
40
73
|
const referenceRecords = await batchReferenceMetadata(source);
|
|
41
|
-
const paths = new Set((await gitRead(source, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']))
|
|
74
|
+
const paths = new Set((await gitRead(source, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']))
|
|
75
|
+
.split('\0')
|
|
76
|
+
.filter(Boolean));
|
|
42
77
|
const files = new Map();
|
|
43
78
|
for (const path of [...paths].sort()) {
|
|
44
79
|
if (!included(path, referenceExamples))
|
|
45
80
|
continue;
|
|
46
81
|
const full = join(source, path);
|
|
47
|
-
const stat = await lstat(full).catch(error => {
|
|
82
|
+
const stat = await lstat(full).catch((error) => {
|
|
48
83
|
if (error.code === 'ENOENT')
|
|
49
84
|
return undefined;
|
|
50
85
|
throw error;
|
|
@@ -73,8 +108,10 @@ export async function readSourceFiles(root) {
|
|
|
73
108
|
// A damaged reference may make an otherwise allowed example fail verification. That
|
|
74
109
|
// is a reference-integrity error, not evidence that a protected path was withheld.
|
|
75
110
|
const isExample = /^\.env\.(?:example|sample|template)$/i.test(path.split('/').pop() ?? '');
|
|
76
|
-
if (paths.has(path) &&
|
|
77
|
-
|
|
111
|
+
if (paths.has(path) &&
|
|
112
|
+
!included(path, referenceExamples) &&
|
|
113
|
+
(!isExample || !included(path, new Set([path]))) &&
|
|
114
|
+
(await lstat(join(source, path)).catch(() => undefined))?.isFile()) {
|
|
78
115
|
withheld.push(record.referenceId);
|
|
79
116
|
break;
|
|
80
117
|
}
|
|
@@ -83,10 +120,15 @@ export async function readSourceFiles(root) {
|
|
|
83
120
|
delete manifest.batchPolicyWithheldReferenceIds;
|
|
84
121
|
if (withheld.length)
|
|
85
122
|
manifest.batchPolicyWithheldReferenceIds = withheld;
|
|
86
|
-
files.set(WORKSPACE_REMIX_REFERENCES_MANIFEST, {
|
|
123
|
+
files.set(WORKSPACE_REMIX_REFERENCES_MANIFEST, {
|
|
124
|
+
...manifestFile,
|
|
125
|
+
body: Buffer.from(JSON.stringify(manifest)),
|
|
126
|
+
});
|
|
87
127
|
}
|
|
88
128
|
}
|
|
89
|
-
catch {
|
|
129
|
+
catch {
|
|
130
|
+
/* Preserve malformed legacy metadata without letting it block an unrelated batch. */
|
|
131
|
+
}
|
|
90
132
|
}
|
|
91
133
|
return files;
|
|
92
134
|
}
|
|
@@ -102,8 +144,121 @@ export async function filterUnstableFiles(root, files) {
|
|
|
102
144
|
}
|
|
103
145
|
return files;
|
|
104
146
|
}
|
|
147
|
+
const scaffoldFiles = new Set([
|
|
148
|
+
'AGENTS.md',
|
|
149
|
+
'.gitignore',
|
|
150
|
+
'.playcraft/skills.json',
|
|
151
|
+
'.playcraft/skills.lock.json',
|
|
152
|
+
'.playcraft/workspace-context.json',
|
|
153
|
+
'.playcraft/workspace.json',
|
|
154
|
+
WORKSPACE_REMIX_REFERENCES_MANIFEST,
|
|
155
|
+
]);
|
|
156
|
+
const scaffoldDirectories = [
|
|
157
|
+
'.agents/skills/',
|
|
158
|
+
'.playcraft/skills/',
|
|
159
|
+
'.playcraft/skill-artifacts/',
|
|
160
|
+
'.playcraft/skill-migrations/',
|
|
161
|
+
'.playcraft/skill-selections/',
|
|
162
|
+
'references/remixes/',
|
|
163
|
+
];
|
|
164
|
+
const ignoredDirectories = [
|
|
165
|
+
'node_modules/',
|
|
166
|
+
'dist/',
|
|
167
|
+
'dist-all/',
|
|
168
|
+
'coverage/',
|
|
169
|
+
'.playcraft-preview-out/',
|
|
170
|
+
'.playcraft-temp/',
|
|
171
|
+
'.tmp/',
|
|
172
|
+
'.temp/',
|
|
173
|
+
'.git/',
|
|
174
|
+
'.vscode/',
|
|
175
|
+
'.idea/',
|
|
176
|
+
];
|
|
177
|
+
function scaffoldPath(path) {
|
|
178
|
+
return scaffoldFiles.has(path) || scaffoldDirectories.some((prefix) => path.startsWith(prefix));
|
|
179
|
+
}
|
|
180
|
+
async function inspectSource(root, files) {
|
|
181
|
+
// Inspect ignored paths too: a user's .gitignore may otherwise hide a real project.
|
|
182
|
+
const ignored = (await gitRead(root, [
|
|
183
|
+
'ls-files',
|
|
184
|
+
'--others',
|
|
185
|
+
'--ignored',
|
|
186
|
+
'--exclude-standard',
|
|
187
|
+
'--directory',
|
|
188
|
+
'-z',
|
|
189
|
+
]))
|
|
190
|
+
.split('\0')
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
const safeIgnored = ignored.every((path) => scaffoldPath(path) ||
|
|
193
|
+
ignoredDirectories.some((prefix) => path === prefix || path.startsWith(prefix)) ||
|
|
194
|
+
!included(path, new Set()) ||
|
|
195
|
+
/(?:^|\/)(?:\.DS_Store|Thumbs\.db)$/.test(path) ||
|
|
196
|
+
/\.(?:log|tmp)$/.test(path));
|
|
197
|
+
return {
|
|
198
|
+
empty: [...files.keys()].every(scaffoldPath),
|
|
199
|
+
safeIgnored,
|
|
200
|
+
ignoredDigest: createHash('sha256').update(JSON.stringify(ignored.sort())).digest('hex'),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Merge only the trusted engine files. Workspace instructions, skills, references and configuration remain authoritative. */
|
|
204
|
+
export function mergeStarterTemplate(files, template) {
|
|
205
|
+
if ('error' in template)
|
|
206
|
+
throw new Error(template.error);
|
|
207
|
+
const result = workspaceStarterTemplateSnapshotSchema.safeParse(template.snapshot);
|
|
208
|
+
if (!result.success)
|
|
209
|
+
throw new Error('starter_template_invalid');
|
|
210
|
+
const snapshot = result.data;
|
|
211
|
+
if (createHash('sha256').update(remixReferenceContentHashInput(snapshot.files)).digest('hex') !==
|
|
212
|
+
snapshot.contentHash)
|
|
213
|
+
throw new Error('starter_template_invalid');
|
|
214
|
+
if (createHash('sha256').update(starterTemplateTreeHashInput(snapshot.files)).digest('hex') !==
|
|
215
|
+
template.treeHash)
|
|
216
|
+
throw new Error('starter_template_invalid');
|
|
217
|
+
const sourcePaths = new Set([...files.keys()].map((path) => path.toLowerCase()));
|
|
218
|
+
const added = [];
|
|
219
|
+
for (const file of snapshot.files) {
|
|
220
|
+
const { path } = file;
|
|
221
|
+
const mode = file.mode;
|
|
222
|
+
if (mode !== '100644' && mode !== '100755')
|
|
223
|
+
throw new Error('starter_template_invalid');
|
|
224
|
+
const body = Buffer.from(file.content, 'base64');
|
|
225
|
+
if (body.toString('base64') !== file.content ||
|
|
226
|
+
body.length !== file.size ||
|
|
227
|
+
createHash('sha256').update(body).digest('hex') !== file.sha256 ||
|
|
228
|
+
body
|
|
229
|
+
.subarray(0, 128)
|
|
230
|
+
.toString('utf8')
|
|
231
|
+
.startsWith('version https://git-lfs.github.com/spec/v1'))
|
|
232
|
+
throw new Error('starter_template_invalid');
|
|
233
|
+
const parts = path.split('/');
|
|
234
|
+
if (path === 'AGENTS.md' ||
|
|
235
|
+
parts[0] === '.claude' ||
|
|
236
|
+
parts[0] === '.agents' ||
|
|
237
|
+
parts[0] === '.playcraft' ||
|
|
238
|
+
parts[0] === 'references' ||
|
|
239
|
+
!included(path, new Set()))
|
|
240
|
+
continue;
|
|
241
|
+
if (path === '.gitignore' && files.has(path)) {
|
|
242
|
+
files.set(path, {
|
|
243
|
+
body: Buffer.concat([files.get(path).body, Buffer.from('\n'), body]),
|
|
244
|
+
mode: 0o644,
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (sourcePaths.has(path.toLowerCase()) ||
|
|
249
|
+
[...sourcePaths].some((existing) => existing.startsWith(`${path.toLowerCase()}/`) ||
|
|
250
|
+
path.toLowerCase().startsWith(`${existing}/`)))
|
|
251
|
+
throw new Error('starter_template_path_conflict');
|
|
252
|
+
files.set(path, { body, mode: mode === '100755' ? 0o755 : 0o644 });
|
|
253
|
+
sourcePaths.add(path.toLowerCase());
|
|
254
|
+
added.push(path);
|
|
255
|
+
}
|
|
256
|
+
if (!added.length)
|
|
257
|
+
throw new Error('starter_template_invalid');
|
|
258
|
+
return added;
|
|
259
|
+
}
|
|
105
260
|
/** Independent root commit. The source branch, index and config are never written. */
|
|
106
|
-
export async function captureIndependentSource(source, destination) {
|
|
261
|
+
export async function captureIndependentSource(source, destination, starterTemplate, identity) {
|
|
107
262
|
const root = await realpath(source);
|
|
108
263
|
if (await lstat(destination).catch(() => undefined))
|
|
109
264
|
throw new Error('batch_source_destination_exists');
|
|
@@ -111,6 +266,56 @@ export async function captureIndependentSource(source, destination) {
|
|
|
111
266
|
let files = await readSourceFiles(root);
|
|
112
267
|
files = await filterUnstableFiles(root, files);
|
|
113
268
|
const contentDigest = digestFiles(files);
|
|
269
|
+
const inspection = starterTemplate ? await inspectSource(root, files) : undefined;
|
|
270
|
+
const probePath = `${destination}.template-probe`;
|
|
271
|
+
const probe = identity && inspection
|
|
272
|
+
? JSON.stringify({
|
|
273
|
+
version: 1,
|
|
274
|
+
identity,
|
|
275
|
+
root,
|
|
276
|
+
head: headBefore,
|
|
277
|
+
contentDigest,
|
|
278
|
+
ignoredDigest: inspection.ignoredDigest,
|
|
279
|
+
})
|
|
280
|
+
: undefined;
|
|
281
|
+
const existingProbe = await readFile(probePath, 'utf8').catch((error) => {
|
|
282
|
+
if (error.code === 'ENOENT')
|
|
283
|
+
return undefined;
|
|
284
|
+
throw error;
|
|
285
|
+
});
|
|
286
|
+
if (identity && starterTemplate && 'snapshot' in starterTemplate && !existingProbe)
|
|
287
|
+
throw new Error('batch_source_candidate_lost');
|
|
288
|
+
if (existingProbe && (!probe || existingProbe !== probe))
|
|
289
|
+
throw new Error('batch_source_changed');
|
|
290
|
+
if (inspection && !inspection.safeIgnored)
|
|
291
|
+
throw new Error('batch_source_ignored_files');
|
|
292
|
+
if (starterTemplate && inspection?.empty && 'error' in starterTemplate && probe) {
|
|
293
|
+
if (headBefore !== (await gitRead(root, ['rev-parse', '--verify', 'HEAD']).catch(() => '')) ||
|
|
294
|
+
digestFiles(await filterUnstableFiles(root, await readSourceFiles(root))) !== contentDigest ||
|
|
295
|
+
(await inspectSource(root, files)).ignoredDigest !== inspection.ignoredDigest)
|
|
296
|
+
throw new Error('batch_source_changed');
|
|
297
|
+
if (!existingProbe) {
|
|
298
|
+
// private
|
|
299
|
+
await mkdir(dirname(probePath), { recursive: true, mode: 0o700 });
|
|
300
|
+
// private
|
|
301
|
+
const handle = await open(probePath, 'wx', 0o600);
|
|
302
|
+
try {
|
|
303
|
+
await handle.writeFile(probe);
|
|
304
|
+
await handle.sync();
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
await handle.close();
|
|
308
|
+
}
|
|
309
|
+
const directory = await open(dirname(probePath), 'r');
|
|
310
|
+
try {
|
|
311
|
+
await directory.sync();
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await directory.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const templatePaths = starterTemplate && inspection?.empty ? mergeStarterTemplate(files, starterTemplate) : [];
|
|
114
319
|
const staging = join(dirname(destination), `.batch-source-${randomUUID()}`);
|
|
115
320
|
try {
|
|
116
321
|
await mkdir(staging, { recursive: true });
|
|
@@ -122,16 +327,35 @@ export async function captureIndependentSource(source, destination) {
|
|
|
122
327
|
await mkdir(dirname(full), { recursive: true });
|
|
123
328
|
await writeFile(full, file.body, { mode: file.mode, flag: 'wx' });
|
|
124
329
|
}
|
|
125
|
-
if (headBefore !== await gitRead(root, ['rev-parse', '--verify', 'HEAD']).catch(() => ''))
|
|
330
|
+
if (headBefore !== (await gitRead(root, ['rev-parse', '--verify', 'HEAD']).catch(() => '')))
|
|
126
331
|
throw new Error('batch_source_changed');
|
|
127
332
|
const after = await filterUnstableFiles(root, await readSourceFiles(root));
|
|
128
333
|
if (digestFiles(after) !== contentDigest)
|
|
129
334
|
throw new Error('batch_source_changed');
|
|
335
|
+
if (inspection) {
|
|
336
|
+
const last = await inspectSource(root, after);
|
|
337
|
+
if (last.empty !== inspection.empty || last.ignoredDigest !== inspection.ignoredDigest)
|
|
338
|
+
throw new Error('batch_source_changed');
|
|
339
|
+
}
|
|
130
340
|
await excludeFromGit(staging);
|
|
131
341
|
await excludeFromGit(staging, '/.env*');
|
|
132
|
-
for (const entry of [
|
|
342
|
+
for (const entry of [
|
|
343
|
+
'/.playcraft.json',
|
|
344
|
+
'/playcraft.config.json',
|
|
345
|
+
'/.playcraft/skills/',
|
|
346
|
+
'/.playcraft-temp/',
|
|
347
|
+
])
|
|
133
348
|
await excludeFromGit(staging, entry);
|
|
134
349
|
await gitWrite(staging, ['add', '-A', '.']);
|
|
350
|
+
// The template owns its source files even when its ignore rules exclude them.
|
|
351
|
+
for (let i = 0; i < templatePaths.length; i += 100)
|
|
352
|
+
await gitWrite(staging, [
|
|
353
|
+
'--literal-pathspecs',
|
|
354
|
+
'add',
|
|
355
|
+
'--force',
|
|
356
|
+
'--',
|
|
357
|
+
...templatePaths.slice(i, i + 100),
|
|
358
|
+
]);
|
|
135
359
|
// Imported reference files can be tracked in the source despite .gitignore. Preserve only
|
|
136
360
|
// their filtered staging paths, without changing ignore handling for other source files.
|
|
137
361
|
for (const path of ['.playcraft/remix-references.json', 'references/remixes']) {
|
|
@@ -151,7 +375,11 @@ export async function archiveSource(root, meta) {
|
|
|
151
375
|
const bundle = join(dirname(root), `.bundle-${randomUUID()}`);
|
|
152
376
|
try {
|
|
153
377
|
await gitWrite(root, ['bundle', 'create', bundle, 'HEAD']);
|
|
154
|
-
const body = JSON.stringify({
|
|
378
|
+
const body = JSON.stringify({
|
|
379
|
+
version: 1,
|
|
380
|
+
...meta,
|
|
381
|
+
bundle: (await readFile(bundle)).toString('base64'),
|
|
382
|
+
});
|
|
155
383
|
return await compress(Buffer.from(body));
|
|
156
384
|
}
|
|
157
385
|
finally {
|
|
@@ -160,7 +388,10 @@ export async function archiveSource(root, meta) {
|
|
|
160
388
|
}
|
|
161
389
|
export async function restoreArchive(body, destination, expectedDigest) {
|
|
162
390
|
const archive = JSON.parse((await decompress(body, { maxOutputLength: 512 * 1024 * 1024 })).toString());
|
|
163
|
-
if (archive.version !== 1 ||
|
|
391
|
+
if (archive.version !== 1 ||
|
|
392
|
+
archive.digest !== expectedDigest ||
|
|
393
|
+
!archive.commitHash ||
|
|
394
|
+
!archive.bundle)
|
|
164
395
|
throw new Error('batch_snapshot_mismatch');
|
|
165
396
|
const temporary = join(dirname(destination), `.bundle-${randomUUID()}`);
|
|
166
397
|
await mkdir(temporary, { recursive: true });
|
|
@@ -31,6 +31,9 @@ function withModelShell(config, modelConfigId) {
|
|
|
31
31
|
return { ...config, 'shell_environment_policy.set': { ...existing, PLAYCRAFT_MODEL_CONFIG_ID: modelConfigId } };
|
|
32
32
|
}
|
|
33
33
|
const ENGINEERING_BATCH_INSTRUCTIONS = [
|
|
34
|
+
'For a single replica, work directly in the current repository and preserve the user-provided references. Do not create a batch for a single replica.',
|
|
35
|
+
'If the repository is empty, initialize it using the default starter template before implementing the replica; preserve existing workspace metadata and files. If the template is unavailable, report the failure instead of substituting another template.',
|
|
36
|
+
'A repository is empty only when it has no user project files, including ignored files; workspace metadata, skills, references and attachments do not count as a project. For an empty repository, run playcraft references prepare-starter-template --json and merge the returned staged template into the current repository. Preserve existing files and workspace configuration, merge .gitignore without replacing it, preserve executable permissions, and report conflicts or preparation failures instead of overwriting files or creating a batch. Once finished, remove only the exact staging directory returned by the command.',
|
|
34
37
|
'When the user requests multiple independent engineering variants, create a real platform batch using playcraft tools create-batch --prompt <the complete request, including constraints established in this conversation> --workspace-id <the trusted batch request workspaceId> --source-directory <the trusted batch request sourceDirectoryName> --client-request-id <the trusted batch request clientRequestId> --json.',
|
|
35
38
|
'Distinguish independent variants from multiple assets, levels or themes within one engineering project. Use the current session directory supplied by the runtime as the batch source; do not infer it from a preview or another conversation.',
|
|
36
39
|
'The platform freezes the current source and generates each planned variant in its own Remix directory. Submission authorizes automatic generation without another prompt approval. Keep the source unchanged for this batch; do not replace independent variants with theme switching in the source.',
|
|
@@ -314,6 +317,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
314
317
|
const developerInstructions = [
|
|
315
318
|
'You coordinate PlayCraft batches. The workspace engineering directories are read-only in this conversation.',
|
|
316
319
|
`Workspace identity: ${scope}. Engineering source root: ${JSON.stringify(cwd)}. Default source directory: basic.`,
|
|
320
|
+
'For a single replica, direct the user to the engineering conversation for the intended repository; do not create a batch or edit an engineering directory from this coordinator.',
|
|
317
321
|
'For a request to create multiple independent variants, create a real platform batch using playcraft tools create-batch --prompt <the complete user request> --workspace-id <workspace identity> --source-directory <basic or the explicitly selected remix directory> --client-request-id <the current batch request context identity> --json.',
|
|
318
322
|
'The platform plans differences, freezes the selected source and generates independent Remix engineering directories. It automatically starts generation; no browser approval is required. Preserve the source files and source Git history.',
|
|
319
323
|
'Do not implement a batch as theme switching inside one engineering directory. Do not generate or repair engineering in this coordinator conversation; the platform production tasks own that work.',
|
|
@@ -416,7 +420,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
416
420
|
}
|
|
417
421
|
async function injectPreparedSkillContext(threadId, prepared) {
|
|
418
422
|
const instructions = asRecord(prepared.params).developerInstructions;
|
|
419
|
-
if (
|
|
423
|
+
if (typeof instructions !== 'string')
|
|
420
424
|
return;
|
|
421
425
|
if (threads.get(threadId)?.turnActive || [...approvals.values()].some(pending => owningThreadId(pending.threadId) === threadId))
|
|
422
426
|
throw new NativePreparationError('skills_reload_pending');
|
|
@@ -425,7 +429,8 @@ export function createCodexAppServerAdapter(options) {
|
|
|
425
429
|
try {
|
|
426
430
|
await rpc.request('thread/inject_items', { threadId, items: [{ type: 'message', role: 'developer', content: [{
|
|
427
431
|
type: 'input_text', text: [
|
|
428
|
-
`Current platform workspace skill lock: ${prepared.skillsHash}. This replaces
|
|
432
|
+
...(prepared.skillsHash ? [`Current platform workspace skill lock: ${prepared.skillsHash}. This replaces earlier platform workspace skill lock and allowlist declarations.`] : []),
|
|
433
|
+
'This replaces earlier platform batch routing instructions and platform starter-template instructions. Keep all other instructions and permissions in force.',
|
|
429
434
|
instructions,
|
|
430
435
|
].join('\n\n'),
|
|
431
436
|
}] }] }, 30_000);
|