@playcraft/cli 0.1.66 → 0.1.68
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/assets/client.js +4 -2
- package/dist/assets/messages.js +2 -0
- package/dist/audio-source/messages.js +35 -0
- package/dist/commands/assets.js +1 -1
- package/dist/commands/audio-program.js +82 -0
- package/dist/commands/audio-source.js +109 -0
- package/dist/commands/audio.js +13 -4
- package/dist/commands/build.js +4 -2
- package/dist/commands/tools-context.js +46 -0
- package/dist/commands/tools.js +2 -0
- package/dist/project-skills/builtin-import.js +69 -22
- package/dist/project-skills/commands.js +11 -2
- package/dist/project-skills/lifecycle.js +1 -1
- package/dist/project-skills/local-store.js +90 -31
- package/dist/project-skills/messages.js +16 -0
- package/dist/project-skills/migrate-lock.js +166 -0
- package/dist/project-skills/remote-cache.js +36 -5
- package/dist/utils/agent-api-client.js +18 -1
- package/dist/utils/agent-generation-provenance.js +8 -2
- package/dist/utils/audio-export.js +249 -0
- package/dist/utils/audio-processor.js +22 -99
- package/dist/utils/execution-context-bridge.js +28 -0
- package/dist/workspace-runtime/batch/control.js +13 -2
- package/dist/workspace-runtime/batch/generate.js +43 -2
- package/dist/workspace-runtime/batch/prepare.js +4 -0
- package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
- package/dist/workspace-runtime/batch/source-capture.js +6 -8
- package/dist/workspace-runtime/codex/app-server.js +26 -2
- package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
- package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
- package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +15 -2
- package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
- package/dist/workspace-runtime/previews/finalize.js +1 -1
- package/dist/workspace-runtime/previews/output.js +4 -1
- package/dist/workspace-runtime/previews/publish.js +5 -2
- package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
- package/dist/workspace-runtime/serve.js +6 -0
- package/dist/workspace-runtime/server/accept-operation.js +85 -13
- package/dist/workspace-runtime/server/audio-assets.js +257 -0
- package/dist/workspace-runtime/server/audio-export.js +136 -0
- package/dist/workspace-runtime/server/directory-access.js +222 -67
- package/dist/workspace-runtime/server/dispatch.js +252 -29
- package/dist/workspace-runtime/server/http.js +97 -3
- package/dist/workspace-runtime/server/operation-record.js +1 -1
- package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
- package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
- package/dist/workspace-runtime/workspaces/asset-library-import.js +27 -7
- package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
- package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
- package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +80 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-import.js +149 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-package.js +26 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-artifact-audit.js +90 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-browser.js +194 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-build-inputs.js +122 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-engine.js +39 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-package.js +289 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-variant-build.js +217 -0
- package/dist/workspace-runtime/workspaces/audio-assets/references.js +107 -0
- package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
- package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
- package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
- package/dist/workspace-runtime/workspaces/audio-assets/service.js +459 -0
- package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
- package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
- package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
- package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
- package/dist/workspace-runtime/workspaces/files.js +3 -5
- package/dist/workspace-runtime/workspaces/git.js +7 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
- package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
- package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
- package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
- package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
- package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
- package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
- package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
- package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
- package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
- package/dist/workspace-runtime/workspaces/skills.js +48 -13
- package/package.json +9 -4
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
- package/project-template/vite.config.ts +1 -1
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template-v2/.playcraft/skills.lock.json +9 -2
- package/project-template-v2/vite.config.ts +1 -1
- package/project-templates/empty/AGENTS.md +2 -0
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/phaser/vite.config.ts +1 -1
- package/project-templates/playable-template-phaser/vite.config.ts +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/threejs/vite.config.ts +1 -1
- package/templates/preview-runtime/v1/README.md +12 -0
- package/templates/preview-runtime/v1/build-preview.mjs +17 -8
- package/templates/preview-runtime/v1/manifest.json +9 -6
- package/templates/preview-runtime/v1/release-guard.mjs +1 -1
- package/templates/preview-runtime/v1/runtime.d.mts +1 -3
- package/templates/preview-runtime/v1/runtime.d.ts +1 -3
- package/templates/preview-runtime/v1/runtime.mjs +1142 -257
- package/templates/preview-runtime/v1/types/host.d.ts +29 -0
- package/templates/preview-runtime/v1/types/index.d.ts +6 -0
- package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import JSON5 from 'json5';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { compileLevelEditorDefinition, applyLevelEditorCommand, applyLevelEditorHistory, validateLevelEditorDocuments, levelEditorCanonicalJson, levelEditorSnapshotSchema, levelEditorTransactionRequestSchema, workspaceRelativePathSchema, WORKSPACE_FILE_CONTENT_MAX, LevelEditorError, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
|
+
import { readParameterAgentFile, parameterInputSnapshot } from './parameter-agent-inputs.js';
|
|
8
|
+
import { resolveWorkspaceFile } from './files.js';
|
|
9
|
+
import { openSharedFile } from './shared-write.js';
|
|
10
|
+
import { parseLevelEditorDocument, serializeLevelEditorDocument } from './level-editor-format.js';
|
|
11
|
+
import { levelEditorExtensionPaths, validateLevelEditorExtensions, } from './level-editor-extensions.js';
|
|
12
|
+
import { recoverPendingLevelEditorTransaction, levelEditorContext, registerLevelEditorContext, transactLevelEditorBatch, } from './level-editor-atomic.js';
|
|
13
|
+
import { readLevelEditorHistory, writeLevelEditorHistory, levelEditorHistoryRequestDigest, assertLevelEditorHistoryScope, } from './level-editor-history-store.js';
|
|
14
|
+
export const levelEditorDigest = (value) => createHash('sha256').update(value).digest('hex');
|
|
15
|
+
const location = z
|
|
16
|
+
.object({
|
|
17
|
+
version: z.literal(1),
|
|
18
|
+
definition: z.string().refine((value) => workspaceRelativePathSchema.safeParse(value).success),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
const empty = { documents: {}, schemas: {}, revisions: {}, documentHashes: {}, diagnostics: [] };
|
|
22
|
+
function semanticViews(root) {
|
|
23
|
+
const containers = new Set(['split@1', 'stack@1', 'tabs@1', 'group@1', 'toolbar@1']);
|
|
24
|
+
const result = [];
|
|
25
|
+
const visit = (node) => {
|
|
26
|
+
if (!containers.has(node.component)) {
|
|
27
|
+
const props = { ...node.props };
|
|
28
|
+
delete props.titleKey;
|
|
29
|
+
delete props.descriptionKey;
|
|
30
|
+
if (node.component === 'inspector@1' && Array.isArray(props.fields)) {
|
|
31
|
+
props.fields = props.fields
|
|
32
|
+
.map((item) => {
|
|
33
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
34
|
+
return item;
|
|
35
|
+
const field = { ...item };
|
|
36
|
+
delete field.titleKey;
|
|
37
|
+
delete field.descriptionKey;
|
|
38
|
+
return field;
|
|
39
|
+
})
|
|
40
|
+
.sort((a, b) => levelEditorCanonicalJson(a).localeCompare(levelEditorCanonicalJson(b)));
|
|
41
|
+
}
|
|
42
|
+
result.push({
|
|
43
|
+
id: node.id,
|
|
44
|
+
component: node.component,
|
|
45
|
+
bind: node.bind ?? {},
|
|
46
|
+
actions: node.actions ?? {},
|
|
47
|
+
props,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
node.children?.forEach(visit);
|
|
51
|
+
};
|
|
52
|
+
visit(root);
|
|
53
|
+
return result.sort((a, b) => a.id.localeCompare(b.id));
|
|
54
|
+
}
|
|
55
|
+
export async function readLevelEditorFile(root, path, limit = WORKSPACE_FILE_CONTENT_MAX) {
|
|
56
|
+
try {
|
|
57
|
+
return await readParameterAgentFile(root, path, limit);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code === 'ENOENT')
|
|
61
|
+
throw error;
|
|
62
|
+
throw new LevelEditorError('level_editor_file_invalid', [path]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function readWorkspaceLevelEditor(root, revisions, context) {
|
|
66
|
+
const access = levelEditorContext(root, context);
|
|
67
|
+
if (access) {
|
|
68
|
+
registerLevelEditorContext(root, access);
|
|
69
|
+
try {
|
|
70
|
+
await recoverPendingLevelEditorTransaction(root, access, () => readLevelEditorSnapshot(root, revisions));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const code = error instanceof LevelEditorError ? error.code : 'level_editor_recovery_required';
|
|
74
|
+
return {
|
|
75
|
+
status: 'invalid',
|
|
76
|
+
...empty,
|
|
77
|
+
diagnostics: [{ code, message: code, severity: 'error' }],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const snapshot = await readLevelEditorSnapshot(root, revisions);
|
|
82
|
+
if (snapshot.status !== 'ready' || !snapshot.definition)
|
|
83
|
+
return snapshot;
|
|
84
|
+
try {
|
|
85
|
+
const diagnostics = [
|
|
86
|
+
...snapshot.diagnostics,
|
|
87
|
+
...(await validateLevelEditorExtensions(root, snapshot.definition, snapshot.documents)),
|
|
88
|
+
];
|
|
89
|
+
if (diagnostics.length > 256)
|
|
90
|
+
throw new LevelEditorError('level_editor_extension_output_invalid');
|
|
91
|
+
return {
|
|
92
|
+
...snapshot,
|
|
93
|
+
status: diagnostics.some((item) => item.severity === 'error') ? 'invalid' : 'ready',
|
|
94
|
+
diagnostics,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const code = error instanceof LevelEditorError ? error.code : 'level_editor_extension_failed';
|
|
99
|
+
return {
|
|
100
|
+
...snapshot,
|
|
101
|
+
status: 'invalid',
|
|
102
|
+
diagnostics: [
|
|
103
|
+
{ code, message: code, severity: 'error' },
|
|
104
|
+
...snapshot.diagnostics,
|
|
105
|
+
].slice(0, 256),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function readLevelEditorSnapshot(root, revisions) {
|
|
110
|
+
try {
|
|
111
|
+
let manifest;
|
|
112
|
+
try {
|
|
113
|
+
manifest = await readLevelEditorFile(root, '.playcraft/workspace.json');
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.code === 'ENOENT')
|
|
117
|
+
return { status: 'absent', ...empty };
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const workspaceManifest = JSON5.parse(manifest.toString('utf8'));
|
|
121
|
+
const raw = workspaceManifest.levelEditor;
|
|
122
|
+
if (raw === undefined)
|
|
123
|
+
return { status: 'absent', ...empty };
|
|
124
|
+
const declared = location.parse(raw);
|
|
125
|
+
const definitionBytes = await readLevelEditorFile(root, declared.definition);
|
|
126
|
+
const initial = compileLevelEditorDefinition(JSON5.parse(definitionBytes.toString('utf8')));
|
|
127
|
+
if (!initial.ok)
|
|
128
|
+
return {
|
|
129
|
+
status: 'invalid',
|
|
130
|
+
...empty,
|
|
131
|
+
definitionPath: declared.definition,
|
|
132
|
+
diagnostics: initial.diagnostics,
|
|
133
|
+
};
|
|
134
|
+
const definition = initial.definition;
|
|
135
|
+
const documents = {}, schemas = {};
|
|
136
|
+
const documentHashes = {}, dataRevisions = {};
|
|
137
|
+
const sourceHashes = [];
|
|
138
|
+
for (const [id, document] of Object.entries(definition.documents)) {
|
|
139
|
+
const [data, schema] = await Promise.all([
|
|
140
|
+
readLevelEditorFile(root, document.path),
|
|
141
|
+
readLevelEditorFile(root, document.schema),
|
|
142
|
+
]);
|
|
143
|
+
let text;
|
|
144
|
+
try {
|
|
145
|
+
text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(data);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new LevelEditorError('level_editor_document_invalid_utf8', [id]);
|
|
149
|
+
}
|
|
150
|
+
documents[id] = await parseLevelEditorDocument(root, document, text);
|
|
151
|
+
schemas[id] = JSON5.parse(schema.toString('utf8'));
|
|
152
|
+
documentHashes[id] = levelEditorDigest(data);
|
|
153
|
+
dataRevisions[id] = revisions[document.path]?.revision ?? 0;
|
|
154
|
+
sourceHashes.push([document.schema, levelEditorDigest(schema)]);
|
|
155
|
+
}
|
|
156
|
+
const checked = compileLevelEditorDefinition(definition, schemas);
|
|
157
|
+
const diagnostics = checked.diagnostics.concat(validateLevelEditorDocuments(definition, documents, schemas));
|
|
158
|
+
const { view: _view, labels: _labels, preview: _preview, ...bindingDefinition } = definition;
|
|
159
|
+
const bindingContractHash = levelEditorDigest(levelEditorCanonicalJson({ ...bindingDefinition, views: semanticViews(definition.view) }));
|
|
160
|
+
const schemaHash = levelEditorDigest(levelEditorCanonicalJson(sourceHashes.sort()));
|
|
161
|
+
const adapterInputs = [];
|
|
162
|
+
for (const path of levelEditorExtensionPaths(definition))
|
|
163
|
+
adapterInputs.push([path, levelEditorDigest(await readLevelEditorFile(root, path))]);
|
|
164
|
+
for (const path of [...new Set(Object.values(definition.preview ?? {}))])
|
|
165
|
+
adapterInputs.push([path, levelEditorDigest(await readLevelEditorFile(root, path))]);
|
|
166
|
+
const authoredData = new Set([
|
|
167
|
+
declared.definition,
|
|
168
|
+
'.playcraft/workspace.json',
|
|
169
|
+
...Object.values(definition.documents).flatMap((document) => [
|
|
170
|
+
document.path,
|
|
171
|
+
document.schema,
|
|
172
|
+
]),
|
|
173
|
+
]);
|
|
174
|
+
// Conservatively cover authored transitive imports, instead of trusting an agent-maintained import list.
|
|
175
|
+
const sourceInputs = (await parameterInputSnapshot(root)).sources.filter((item) => !authoredData.has(item.path));
|
|
176
|
+
for (const path of workspaceManifest.previewRuntime?.gameFilePaths ?? []) {
|
|
177
|
+
workspaceRelativePathSchema.parse(path);
|
|
178
|
+
if (!authoredData.has(path))
|
|
179
|
+
adapterInputs.push([path, levelEditorDigest(await readLevelEditorFile(root, path))]);
|
|
180
|
+
}
|
|
181
|
+
const executionContractHash = levelEditorDigest(levelEditorCanonicalJson({
|
|
182
|
+
bindingContractHash,
|
|
183
|
+
schemaHash,
|
|
184
|
+
adapterInputs,
|
|
185
|
+
sourceInputs,
|
|
186
|
+
previewRuntime: workspaceManifest.previewRuntime ?? null,
|
|
187
|
+
}));
|
|
188
|
+
const sourceFingerprint = levelEditorDigest(levelEditorCanonicalJson({
|
|
189
|
+
definitionPath: declared.definition,
|
|
190
|
+
bindingContractHash,
|
|
191
|
+
schemaHash,
|
|
192
|
+
executionContractHash,
|
|
193
|
+
}));
|
|
194
|
+
return levelEditorSnapshotSchema.parse({
|
|
195
|
+
status: diagnostics.some((item) => item.severity === 'error') ? 'invalid' : 'ready',
|
|
196
|
+
definitionPath: declared.definition,
|
|
197
|
+
definition,
|
|
198
|
+
documents,
|
|
199
|
+
schemas,
|
|
200
|
+
revisions: dataRevisions,
|
|
201
|
+
documentHashes,
|
|
202
|
+
definitionHash: levelEditorDigest(definitionBytes),
|
|
203
|
+
sourceFingerprint,
|
|
204
|
+
bindingContractHash,
|
|
205
|
+
schemaHash,
|
|
206
|
+
executionContractHash,
|
|
207
|
+
diagnostics,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const code = error instanceof LevelEditorError ? error.code : 'level_editor_document_invalid';
|
|
212
|
+
const diagnostics = [{ code, message: code, severity: 'error' }];
|
|
213
|
+
return { status: 'invalid', ...empty, diagnostics };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function transactWorkspaceLevelEditor(root, revisions, value, context) {
|
|
217
|
+
const request = levelEditorTransactionRequestSchema.parse(value);
|
|
218
|
+
if (request.version === 2)
|
|
219
|
+
return transactLevelEditorBatch(root, revisions, request, context, () => readLevelEditorSnapshot(root, revisions));
|
|
220
|
+
if (context) {
|
|
221
|
+
assertLevelEditorHistoryScope(context);
|
|
222
|
+
registerLevelEditorContext(root, context);
|
|
223
|
+
}
|
|
224
|
+
if (context && context.operation.clientRequestId !== request.requestId)
|
|
225
|
+
throw new LevelEditorError('level_editor_history_scope_invalid');
|
|
226
|
+
const snapshot = await readWorkspaceLevelEditor(root, revisions);
|
|
227
|
+
if (snapshot.status !== 'ready' || !snapshot.definition)
|
|
228
|
+
throw new LevelEditorError('level_editor_not_ready');
|
|
229
|
+
if (snapshot.definition.editorId !== request.editorId ||
|
|
230
|
+
snapshot.definition.dataContractVersion !== request.dataContractVersion)
|
|
231
|
+
throw new LevelEditorError('level_editor_contract_conflict');
|
|
232
|
+
for (const key of [
|
|
233
|
+
'sourceFingerprint',
|
|
234
|
+
'bindingContractHash',
|
|
235
|
+
'schemaHash',
|
|
236
|
+
'executionContractHash',
|
|
237
|
+
])
|
|
238
|
+
if (snapshot[key] !== request[key])
|
|
239
|
+
throw new LevelEditorError('level_editor_source_conflict');
|
|
240
|
+
const previousReceipt = context && (await readLevelEditorHistory(root, context, request.requestId));
|
|
241
|
+
if (previousReceipt) {
|
|
242
|
+
if (previousReceipt.requestDigest !== levelEditorHistoryRequestDigest(request))
|
|
243
|
+
throw new LevelEditorError('level_editor_history_request_conflict');
|
|
244
|
+
const currentRevision = snapshot.revisions[request.documentId];
|
|
245
|
+
if (snapshot.documentHashes[request.documentId] === previousReceipt.contentHash &&
|
|
246
|
+
(currentRevision === request.expectedRevision ||
|
|
247
|
+
currentRevision === request.expectedRevision + 1)) {
|
|
248
|
+
if (snapshot.definition.documents[request.documentId]?.path !== previousReceipt.path)
|
|
249
|
+
throw new LevelEditorError('level_editor_history_invalid');
|
|
250
|
+
await writeLevelEditorHistory(root, context, { ...previousReceipt, phase: 'confirmed' });
|
|
251
|
+
return {
|
|
252
|
+
path: previousReceipt.path,
|
|
253
|
+
contentHash: previousReceipt.contentHash,
|
|
254
|
+
size: previousReceipt.size,
|
|
255
|
+
revision: request.expectedRevision + 1,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (previousReceipt.phase === 'confirmed')
|
|
259
|
+
throw new LevelEditorError('revision_conflict');
|
|
260
|
+
}
|
|
261
|
+
if (snapshot.revisions[request.documentId] !== request.expectedRevision ||
|
|
262
|
+
snapshot.documentHashes[request.documentId] !== request.expectedDocumentHash)
|
|
263
|
+
throw new LevelEditorError('revision_conflict');
|
|
264
|
+
const command = {
|
|
265
|
+
commandId: request.commandId,
|
|
266
|
+
selection: request.selection,
|
|
267
|
+
input: request.input,
|
|
268
|
+
};
|
|
269
|
+
const originalReceipt = request.history &&
|
|
270
|
+
context &&
|
|
271
|
+
(await readLevelEditorHistory(root, context, request.history.requestId));
|
|
272
|
+
if (request.history) {
|
|
273
|
+
if (originalReceipt?.request.version !== 1)
|
|
274
|
+
throw new LevelEditorError('level_editor_history_missing');
|
|
275
|
+
if (!originalReceipt ||
|
|
276
|
+
originalReceipt.phase !== 'confirmed' ||
|
|
277
|
+
originalReceipt.request.history ||
|
|
278
|
+
request.history.requestId === request.requestId)
|
|
279
|
+
throw new LevelEditorError('level_editor_history_missing');
|
|
280
|
+
for (const key of [
|
|
281
|
+
'editorId',
|
|
282
|
+
'dataContractVersion',
|
|
283
|
+
'documentId',
|
|
284
|
+
'sourceFingerprint',
|
|
285
|
+
'bindingContractHash',
|
|
286
|
+
'schemaHash',
|
|
287
|
+
'executionContractHash',
|
|
288
|
+
])
|
|
289
|
+
if (originalReceipt.request[key] !==
|
|
290
|
+
request[key])
|
|
291
|
+
throw new LevelEditorError('level_editor_source_conflict');
|
|
292
|
+
if (levelEditorCanonicalJson(originalReceipt.command) !== levelEditorCanonicalJson(command))
|
|
293
|
+
throw new LevelEditorError('level_editor_history_invalid');
|
|
294
|
+
}
|
|
295
|
+
const outcome = request.history
|
|
296
|
+
? applyLevelEditorHistory(snapshot.definition, snapshot.documents, originalReceipt, request.history.direction, snapshot.schemas)
|
|
297
|
+
: applyLevelEditorCommand(snapshot.definition, snapshot.documents, command, snapshot.schemas);
|
|
298
|
+
if (outcome.documentId !== request.documentId)
|
|
299
|
+
throw new LevelEditorError('level_editor_document_mismatch');
|
|
300
|
+
const document = snapshot.definition.documents[request.documentId];
|
|
301
|
+
const original = await readLevelEditorFile(root, document.path);
|
|
302
|
+
if (levelEditorDigest(original) !== request.expectedDocumentHash)
|
|
303
|
+
throw new LevelEditorError('revision_conflict');
|
|
304
|
+
const candidate = { ...snapshot.documents, [outcome.documentId]: outcome.data };
|
|
305
|
+
if ((await validateLevelEditorExtensions(root, snapshot.definition, candidate)).some((item) => item.severity === 'error'))
|
|
306
|
+
throw new LevelEditorError('level_editor_validation_failed');
|
|
307
|
+
const source = original.toString('utf8');
|
|
308
|
+
const edited = await serializeLevelEditorDocument(root, document, source, outcome.changes, outcome.data);
|
|
309
|
+
if (!document.provider && document.format !== 'yaml' && document.path.endsWith('.json'))
|
|
310
|
+
JSON.parse(edited);
|
|
311
|
+
const bytes = Buffer.from(edited);
|
|
312
|
+
if (bytes.length > WORKSPACE_FILE_CONTENT_MAX)
|
|
313
|
+
throw new LevelEditorError('level_editor_document_budget');
|
|
314
|
+
const path = await resolveWorkspaceFile(root, document.path);
|
|
315
|
+
const previous = await lstat(path);
|
|
316
|
+
if (!previous.isFile() || previous.isSymbolicLink() || previous.nlink !== 1)
|
|
317
|
+
throw new LevelEditorError('level_editor_file_invalid');
|
|
318
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
319
|
+
const receipt = {
|
|
320
|
+
request,
|
|
321
|
+
requestDigest: levelEditorHistoryRequestDigest(request),
|
|
322
|
+
phase: 'prepared',
|
|
323
|
+
command,
|
|
324
|
+
documentId: request.documentId,
|
|
325
|
+
before: snapshot.documents[request.documentId],
|
|
326
|
+
after: outcome.data,
|
|
327
|
+
path: document.path,
|
|
328
|
+
contentHash: levelEditorDigest(bytes),
|
|
329
|
+
size: bytes.length,
|
|
330
|
+
};
|
|
331
|
+
if (context)
|
|
332
|
+
await writeLevelEditorHistory(root, context, receipt);
|
|
333
|
+
try {
|
|
334
|
+
const handle = await openSharedFile(root, temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW);
|
|
335
|
+
try {
|
|
336
|
+
await handle.writeFile(bytes);
|
|
337
|
+
await handle.sync();
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
await handle.close();
|
|
341
|
+
}
|
|
342
|
+
if (context)
|
|
343
|
+
assertLevelEditorHistoryScope(context);
|
|
344
|
+
const latest = await readLevelEditorSnapshot(root, revisions);
|
|
345
|
+
if (latest.sourceFingerprint !== snapshot.sourceFingerprint ||
|
|
346
|
+
latest.documentHashes[request.documentId] !== request.expectedDocumentHash)
|
|
347
|
+
throw new LevelEditorError('level_editor_source_conflict');
|
|
348
|
+
if (context) {
|
|
349
|
+
assertLevelEditorHistoryScope(context);
|
|
350
|
+
if (context.state.journal?.destroyed() ||
|
|
351
|
+
(context.operation.leaseFence ?? 0) < context.state.acceptedLeaseFence)
|
|
352
|
+
throw new LevelEditorError('level_editor_source_conflict');
|
|
353
|
+
}
|
|
354
|
+
await rename(temporary, path);
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
await rm(temporary, { force: true });
|
|
358
|
+
}
|
|
359
|
+
if (context) {
|
|
360
|
+
try {
|
|
361
|
+
await writeLevelEditorHistory(root, context, { ...receipt, phase: 'confirmed' });
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
throw new LevelEditorError('level_editor_transaction_uncertain');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
path: document.path,
|
|
369
|
+
contentHash: levelEditorDigest(bytes),
|
|
370
|
+
size: bytes.length,
|
|
371
|
+
revision: request.expectedRevision + 1,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** Reconcile private proof; a batch resumes only its previously authorized durable intent. */
|
|
375
|
+
export async function reconcileWorkspaceLevelEditorTransaction(root, revisions, value, context) {
|
|
376
|
+
const request = levelEditorTransactionRequestSchema.parse(value);
|
|
377
|
+
if (context.operation.clientRequestId !== request.requestId)
|
|
378
|
+
return undefined;
|
|
379
|
+
if (request.version === 2) {
|
|
380
|
+
await recoverPendingLevelEditorTransaction(root, context, () => readLevelEditorSnapshot(root, revisions));
|
|
381
|
+
let receipt = await readLevelEditorHistory(root, context, request.requestId);
|
|
382
|
+
if (receipt?.batch &&
|
|
383
|
+
receipt.phase === 'prepared' &&
|
|
384
|
+
receipt.requestDigest === levelEditorHistoryRequestDigest(request)) {
|
|
385
|
+
await transactLevelEditorBatch(root, revisions, request, context, () => readLevelEditorSnapshot(root, revisions));
|
|
386
|
+
receipt = await readLevelEditorHistory(root, context, request.requestId);
|
|
387
|
+
}
|
|
388
|
+
if (!receipt?.batch ||
|
|
389
|
+
receipt.phase !== 'confirmed' ||
|
|
390
|
+
receipt.batch.operationId !== context.operation.operationId ||
|
|
391
|
+
receipt.requestDigest !== levelEditorHistoryRequestDigest(request))
|
|
392
|
+
return undefined;
|
|
393
|
+
const snapshot = await readLevelEditorSnapshot(root, revisions);
|
|
394
|
+
if (snapshot.status !== 'ready' ||
|
|
395
|
+
snapshot.definition?.editorId !== request.editorId ||
|
|
396
|
+
snapshot.definition.dataContractVersion !== request.dataContractVersion ||
|
|
397
|
+
['sourceFingerprint', 'bindingContractHash', 'schemaHash', 'executionContractHash'].some((key) => snapshot[key] !== request[key]) ||
|
|
398
|
+
JSON.stringify(Object.keys(snapshot.definition.documents).sort()) !==
|
|
399
|
+
JSON.stringify(Object.keys(request.expectedDocuments).sort()) ||
|
|
400
|
+
Object.entries(receipt.batch.baselines).some(([id, baseline]) => {
|
|
401
|
+
const file = receipt.batch.files.find((file) => file.documentId === id), revision = snapshot.revisions[id];
|
|
402
|
+
return (snapshot.definition.documents[id]?.path !== baseline.path ||
|
|
403
|
+
snapshot.documentHashes[id] !== (file?.contentHash ?? baseline.contentHash) ||
|
|
404
|
+
(file
|
|
405
|
+
? revision !== baseline.revision && revision !== file.revision
|
|
406
|
+
: revision !== baseline.revision));
|
|
407
|
+
}))
|
|
408
|
+
return undefined;
|
|
409
|
+
const results = receipt.batch.files.map(({ documentId, path, contentHash, size, revision }) => ({
|
|
410
|
+
documentId,
|
|
411
|
+
path,
|
|
412
|
+
contentHash,
|
|
413
|
+
size,
|
|
414
|
+
revision,
|
|
415
|
+
}));
|
|
416
|
+
for (const item of results)
|
|
417
|
+
snapshot.revisions[item.documentId] = item.revision;
|
|
418
|
+
return { ...results[0], results, snapshot };
|
|
419
|
+
}
|
|
420
|
+
const receipt = await readLevelEditorHistory(root, context, request.requestId);
|
|
421
|
+
if (!receipt || receipt.requestDigest !== levelEditorHistoryRequestDigest(request))
|
|
422
|
+
return undefined;
|
|
423
|
+
const snapshot = await readWorkspaceLevelEditor(root, revisions);
|
|
424
|
+
if (snapshot.status !== 'ready' ||
|
|
425
|
+
!snapshot.definition ||
|
|
426
|
+
snapshot.definition.editorId !== request.editorId ||
|
|
427
|
+
snapshot.definition.dataContractVersion !== request.dataContractVersion ||
|
|
428
|
+
snapshot.definition.documents[request.documentId]?.path !== receipt.path)
|
|
429
|
+
return undefined;
|
|
430
|
+
for (const key of [
|
|
431
|
+
'sourceFingerprint',
|
|
432
|
+
'bindingContractHash',
|
|
433
|
+
'schemaHash',
|
|
434
|
+
'executionContractHash',
|
|
435
|
+
])
|
|
436
|
+
if (snapshot[key] !== request[key])
|
|
437
|
+
return undefined;
|
|
438
|
+
const revision = snapshot.revisions[request.documentId];
|
|
439
|
+
if (snapshot.documentHashes[request.documentId] !== receipt.contentHash ||
|
|
440
|
+
(revision !== request.expectedRevision && revision !== request.expectedRevision + 1) ||
|
|
441
|
+
levelEditorCanonicalJson(snapshot.documents[request.documentId]) !==
|
|
442
|
+
levelEditorCanonicalJson(receipt.after))
|
|
443
|
+
return undefined;
|
|
444
|
+
await writeLevelEditorHistory(root, context, { ...receipt, phase: 'confirmed' });
|
|
445
|
+
const nextRevision = request.expectedRevision + 1;
|
|
446
|
+
snapshot.revisions[request.documentId] = nextRevision;
|
|
447
|
+
return {
|
|
448
|
+
path: receipt.path,
|
|
449
|
+
contentHash: receipt.contentHash,
|
|
450
|
+
size: receipt.size,
|
|
451
|
+
revision: nextRevision,
|
|
452
|
+
snapshot,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
@@ -8,8 +8,7 @@ import { excludeFromGit } from './dependencies.js';
|
|
|
8
8
|
import { ensureSharedDirectory, openSharedFile, shareIfNeeded } from './shared-write.js';
|
|
9
9
|
import { acquireParameterSkillLease, readParameterSkillTransactionFile, writeParameterSkillTransaction } from './parameter-initialization-skill-transaction.js';
|
|
10
10
|
export const PARAMETER_INITIALIZATION_SKILL = 'playcraft-runtime-parameters';
|
|
11
|
-
const
|
|
12
|
-
const skillRef = `builtin/${PARAMETER_INITIALIZATION_SKILL}`;
|
|
11
|
+
const profile = (name) => ({ sourceRoot: `.agents/skills/${name}`, skillRef: `builtin/${name}` });
|
|
13
12
|
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
14
13
|
async function safeDirectory(root, path) {
|
|
15
14
|
let current = root;
|
|
@@ -48,7 +47,8 @@ async function optionalFile(path) {
|
|
|
48
47
|
throw error;
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
|
-
async function installSource(root, files, token) {
|
|
50
|
+
async function installSource(root, files, token, name) {
|
|
51
|
+
const { sourceRoot } = profile(name);
|
|
52
52
|
const directory = join(root, sourceRoot);
|
|
53
53
|
const staging = await safeDirectory(root, `.playcraft/parameter-initialization-${token}`);
|
|
54
54
|
const temporary = join(staging, 'source');
|
|
@@ -98,9 +98,10 @@ function snapshot(originalDeclaration, originalLock) {
|
|
|
98
98
|
const declaration = { schemaVersion: 1, entries: [] };
|
|
99
99
|
return { declaration, lock: { lockVersion: 3, declarationHash: declarationDigest(declaration), entries: [] } };
|
|
100
100
|
}
|
|
101
|
-
function selectedSnapshot(previous, templateHash, artifactHash) {
|
|
101
|
+
function selectedSnapshot(previous, templateHash, artifactHash, name) {
|
|
102
|
+
const { sourceRoot, skillRef } = profile(name);
|
|
102
103
|
const next = structuredClone(previous);
|
|
103
|
-
const matches = next.declaration.entries.filter(entry => entry.entryKey ===
|
|
104
|
+
const matches = next.declaration.entries.filter(entry => entry.entryKey === name
|
|
104
105
|
|| entry.source.skillRef === skillRef || entry.source.kind === 'repository' && entry.source.root === sourceRoot);
|
|
105
106
|
if (matches.length > 1)
|
|
106
107
|
throw new Error('workspace_skills_parameter_conflict');
|
|
@@ -111,24 +112,52 @@ function selectedSnapshot(previous, templateHash, artifactHash) {
|
|
|
111
112
|
if (existing.source.kind !== 'repository' || existing.source.root !== sourceRoot
|
|
112
113
|
|| existing.source.skillRef !== skillRef || existing.surface !== 'sandbox-agent' || existing.projectFiles?.length
|
|
113
114
|
|| locked.dependencies.length || locked.revision.kind !== 'source' || locked.sourceTreeHash !== templateHash
|
|
114
|
-
|| locked.revision.artifactHash !== artifactHash)
|
|
115
|
+
|| (next.lock.lockVersion === 3 && locked.revision.artifactHash !== artifactHash)
|
|
116
|
+
|| (next.lock.lockVersion === 4 && locked.revision.contentHash !== templateHash))
|
|
115
117
|
throw new Error('workspace_skills_parameter_conflict');
|
|
116
118
|
selected = existing;
|
|
117
119
|
selected.enabled = true;
|
|
118
120
|
}
|
|
119
121
|
else {
|
|
120
|
-
selected = { entryKey:
|
|
122
|
+
selected = { entryKey: name, source: { kind: 'repository', root: sourceRoot, skillRef },
|
|
121
123
|
enabled: true, surface: 'sandbox-agent' };
|
|
122
124
|
const locked = { entryKey: selected.entryKey, sourceTreeHash: templateHash,
|
|
123
|
-
revision: { kind: 'source', skillRef, sourceRevision: `src-v1-sha256:${templateHash}`, artifactHash
|
|
125
|
+
revision: { kind: 'source', skillRef, sourceRevision: `src-v1-sha256:${templateHash}`, artifactHash,
|
|
126
|
+
...(next.lock.lockVersion === 4 ? { contentHash: templateHash } : {}) },
|
|
124
127
|
dependencies: [], dependencyLockHash: dependencyDigest([], []) };
|
|
125
128
|
next.declaration.entries.push(selected);
|
|
126
|
-
next.lock.entries.
|
|
129
|
+
next.lock = { ...next.lock, entries: [...next.lock.entries, locked] };
|
|
127
130
|
}
|
|
128
131
|
next.lock.declarationHash = declarationDigest(next.declaration);
|
|
129
132
|
validateProjectSkillsSnapshot(next.declaration, next.lock);
|
|
130
133
|
return { next, selected, sourceAdded: !existing };
|
|
131
134
|
}
|
|
135
|
+
async function preservedSelectionSource(root, previous, name) {
|
|
136
|
+
const { sourceRoot, skillRef } = profile(name);
|
|
137
|
+
const matches = previous.declaration.entries.filter(entry => entry.entryKey === name || entry.source.skillRef === skillRef
|
|
138
|
+
|| entry.source.kind === 'repository' && entry.source.root === sourceRoot);
|
|
139
|
+
if (matches.length > 1)
|
|
140
|
+
throw new Error('workspace_skills_parameter_conflict');
|
|
141
|
+
const selected = matches[0];
|
|
142
|
+
if (!selected)
|
|
143
|
+
return undefined;
|
|
144
|
+
const locked = previous.lock.entries.find(entry => entry.entryKey === selected.entryKey);
|
|
145
|
+
if (!locked || selected.source.kind !== 'repository' || selected.source.root !== sourceRoot || selected.source.skillRef !== skillRef
|
|
146
|
+
|| selected.surface !== 'sandbox-agent' || selected.projectFiles?.length || locked.dependencies.length
|
|
147
|
+
|| locked.revision.kind !== 'source' || !locked.sourceTreeHash
|
|
148
|
+
|| locked.revision.sourceRevision !== `src-v1-sha256:${locked.sourceTreeHash}`)
|
|
149
|
+
throw new Error('workspace_skills_parameter_conflict');
|
|
150
|
+
const source = await readProjectSkillSource(root, selected);
|
|
151
|
+
if (source.sourceTreeHash !== locked.sourceTreeHash)
|
|
152
|
+
throw new Error('workspace_skills_source_changed');
|
|
153
|
+
const archive = createCanonicalBundleArchive(source.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
|
|
154
|
+
const artifactHash = digest(archive);
|
|
155
|
+
if (previous.lock.lockVersion === 3 && artifactHash !== locked.revision.artifactHash)
|
|
156
|
+
throw new Error('workspace_skills_digest_mismatch');
|
|
157
|
+
if (previous.lock.lockVersion === 4 && source.sourceTreeHash !== locked.revision.contentHash)
|
|
158
|
+
throw new Error('workspace_skills_digest_mismatch');
|
|
159
|
+
return { source, archive, artifactHash };
|
|
160
|
+
}
|
|
132
161
|
async function cleanStaging(root, transaction, files, archive) {
|
|
133
162
|
const directory = join(root, '.playcraft', `parameter-initialization-${transaction.owner.token}`);
|
|
134
163
|
const expected = new Map(files.map(file => [`source/${file.path}`, Buffer.from(file.content)]));
|
|
@@ -168,14 +197,15 @@ async function cleanStaging(root, transaction, files, archive) {
|
|
|
168
197
|
}
|
|
169
198
|
}
|
|
170
199
|
}
|
|
171
|
-
async function recoverTransaction(root, transaction, template, artifactHash, archive) {
|
|
200
|
+
async function recoverTransaction(root, transaction, template, artifactHash, archive, name) {
|
|
201
|
+
const { sourceRoot } = profile(name);
|
|
172
202
|
if (transaction.templateHash !== template.sourceTreeHash || transaction.artifactHash !== artifactHash)
|
|
173
203
|
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
174
204
|
const originalDeclaration = transaction.originalDeclaration === null ? undefined : Buffer.from(transaction.originalDeclaration, 'base64');
|
|
175
205
|
if (originalDeclaration && originalDeclaration.toString('base64') !== transaction.originalDeclaration)
|
|
176
206
|
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
177
207
|
const previous = snapshot(originalDeclaration, transaction.originalLock);
|
|
178
|
-
const { next, selected, sourceAdded } = selectedSnapshot(previous, template.sourceTreeHash, artifactHash);
|
|
208
|
+
const { next, selected, sourceAdded } = selectedSnapshot(previous, template.sourceTreeHash, artifactHash, name);
|
|
179
209
|
if (sourceAdded !== transaction.sourceAdded || `${canonicalProjectSkillsJson(next.declaration)}\n` !== transaction.nextDeclaration
|
|
180
210
|
|| `${canonicalProjectSkillsJson(next.lock)}\n` !== transaction.nextLock)
|
|
181
211
|
throw new Error('workspace_skills_parameter_recovery_conflict');
|
|
@@ -212,22 +242,24 @@ async function recoverTransaction(root, transaction, template, artifactHash, arc
|
|
|
212
242
|
await cleanStaging(root, transaction, template.files, archive);
|
|
213
243
|
}
|
|
214
244
|
/** A deliberate initialization selects one versioned project skill without synchronizing other sources. */
|
|
215
|
-
export async function ensureParameterInitializationSkill(root) {
|
|
245
|
+
export async function ensureParameterInitializationSkill(root, name = PARAMETER_INITIALIZATION_SKILL, options = {}) {
|
|
246
|
+
const { sourceRoot, skillRef } = profile(name);
|
|
216
247
|
const rootStat = await lstat(root);
|
|
217
248
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
218
249
|
throw new Error('workspace_skills_invalid');
|
|
219
|
-
const templateEntry = { entryKey:
|
|
220
|
-
source: { kind: 'repository', root: `project-template/.claude/skills/${
|
|
250
|
+
const templateEntry = { entryKey: name,
|
|
251
|
+
source: { kind: 'repository', root: `project-template/.claude/skills/${name}`, skillRef },
|
|
221
252
|
enabled: true, surface: 'sandbox-agent' };
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
const
|
|
253
|
+
const bundledTemplate = await readProjectSkillSource(resolveCliPackageRoot(), templateEntry);
|
|
254
|
+
const bundledArchive = createCanonicalBundleArchive(bundledTemplate.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
|
|
255
|
+
const bundledArtifactHash = digest(bundledArchive);
|
|
225
256
|
await excludeFromGit(root, '/.playcraft/skills/');
|
|
226
257
|
await excludeFromGit(root, '/.playcraft/skill-artifacts/');
|
|
227
258
|
await excludeFromGit(root, '/.playcraft/skills-write.lock');
|
|
228
259
|
await excludeFromGit(root, '/.playcraft/parameter-initialization-*');
|
|
229
260
|
const management = await safeDirectory(root, '.playcraft');
|
|
230
|
-
const lease = await acquireParameterSkillLease(management, transaction => recoverTransaction(root, transaction,
|
|
261
|
+
const lease = await acquireParameterSkillLease(management, transaction => recoverTransaction(root, transaction, bundledTemplate, bundledArtifactHash, bundledArchive, name));
|
|
262
|
+
let template = bundledTemplate, archive = bundledArchive, artifactHash = bundledArtifactHash;
|
|
231
263
|
const staged = [];
|
|
232
264
|
let ownedSource = false, committed = false, cleanupAllowed = true;
|
|
233
265
|
let removeTransaction;
|
|
@@ -236,7 +268,15 @@ export async function ensureParameterInitializationSkill(root) {
|
|
|
236
268
|
const originalDeclaration = await optionalFile(declarationPath);
|
|
237
269
|
const originalLock = await readProjectSkillsLockFile(root);
|
|
238
270
|
const previous = snapshot(originalDeclaration, originalLock);
|
|
239
|
-
|
|
271
|
+
if (options.preserveExisting) {
|
|
272
|
+
const preserved = await preservedSelectionSource(root, previous, name);
|
|
273
|
+
if (preserved) {
|
|
274
|
+
template = preserved.source;
|
|
275
|
+
archive = preserved.archive;
|
|
276
|
+
artifactHash = preserved.artifactHash;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const { next, selected, sourceAdded } = selectedSnapshot(previous, template.sourceTreeHash, artifactHash, name);
|
|
240
280
|
if (!sourceAdded) {
|
|
241
281
|
if ((await readProjectSkillSource(root, selected)).sourceTreeHash !== template.sourceTreeHash)
|
|
242
282
|
throw new Error('workspace_skills_source_changed');
|
|
@@ -249,7 +289,7 @@ export async function ensureParameterInitializationSkill(root) {
|
|
|
249
289
|
}
|
|
250
290
|
else {
|
|
251
291
|
for (const source of PROJECT_SKILL_SOURCE_ROOTS) {
|
|
252
|
-
const path = join(root, source,
|
|
292
|
+
const path = join(root, source, name);
|
|
253
293
|
const exists = await lstat(path).then(() => true, error => {
|
|
254
294
|
if (error.code === 'ENOENT')
|
|
255
295
|
return false;
|
|
@@ -265,7 +305,7 @@ export async function ensureParameterInitializationSkill(root) {
|
|
|
265
305
|
originalDeclaration: originalDeclaration?.toString('base64') ?? null, originalLock,
|
|
266
306
|
nextDeclaration: `${canonicalProjectSkillsJson(next.declaration)}\n`, nextLock: `${canonicalProjectSkillsJson(next.lock)}\n` });
|
|
267
307
|
if (sourceAdded) {
|
|
268
|
-
await installSource(root, template.files, lease.owner.token);
|
|
308
|
+
await installSource(root, template.files, lease.owner.token, name);
|
|
269
309
|
ownedSource = true;
|
|
270
310
|
}
|
|
271
311
|
await preserveArtifact(root, artifactHash, archive, lease.owner.token);
|
|
@@ -313,7 +353,7 @@ export async function ensureParameterInitializationSkill(root) {
|
|
|
313
353
|
}
|
|
314
354
|
finally {
|
|
315
355
|
if (cleanupAllowed && ownedSource && !committed) {
|
|
316
|
-
const entry = { entryKey:
|
|
356
|
+
const entry = { entryKey: name,
|
|
317
357
|
source: { kind: 'repository', root: sourceRoot }, enabled: true, surface: 'sandbox-agent' };
|
|
318
358
|
if ((await readProjectSkillSource(root, entry)).sourceTreeHash !== template.sourceTreeHash)
|
|
319
359
|
throw new Error('workspace_skills_source_changed');
|
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { workspaceRelativePathSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
6
|
import { ParameterAgentError, parameterDigest, readParameterAgentFile } from './parameter-agent-inputs.js';
|
|
7
7
|
const reportPath = '.playcraft-editor-preview/release-report.json';
|
|
8
|
-
const marker = /runtime\.parameters\.|playcraft-preview-storage:|
|
|
8
|
+
const marker = /runtime\.(?:parameters|levelEditor|level-editor)\.|playcraft-preview-storage:|__playcraft_(?:preview|level_editor)_context__|playcraft-level-editor-profile/i;
|
|
9
9
|
const slash = (value) => value.replaceAll('\\', '/').replaceAll('\0', '').split('?')[0];
|
|
10
10
|
const relativePath = z.string().refine(value => workspaceRelativePathSchema.safeParse(value).success);
|
|
11
11
|
const reportSchema = z.object({
|