@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertLeaseFence, hashOperationPayload, isLostThreadError, operationScopeKey, resolveIdempotentAccept, runtimeError, takesWorkspaceDirectory, isTurnControl, } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
2
|
import { canExecuteKind, executeOperation } from './dispatch.js';
|
|
3
3
|
import { buildOperationRecord } from './operation-record.js';
|
|
4
|
+
import { preflightSkillExecution } from '../workspaces/skill-execution-context.js';
|
|
4
5
|
/**
|
|
5
6
|
* Shared operation acceptance: lease fence, idempotency, capability, record creation and
|
|
6
7
|
* execution. HTTP supplies its transport/session checks through `validate`; in-sandbox Agent
|
|
@@ -62,7 +63,15 @@ export async function acceptOperationCore(state, parsed, options) {
|
|
|
62
63
|
if (validation)
|
|
63
64
|
return { rejected: true, refusal: validation };
|
|
64
65
|
const executable = canExecuteKind(state, parsed.kind);
|
|
65
|
-
|
|
66
|
+
let policy;
|
|
67
|
+
try {
|
|
68
|
+
policy = await preflightSkillExecution(state, parsed);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return rejected(409, { error: error instanceof Error && /^(?:context_|invalid_composer_context|untrusted_runtime_field)/.test(error.message)
|
|
72
|
+
? error.message : 'context_skill_unavailable' });
|
|
73
|
+
}
|
|
74
|
+
const operation = buildOperationRecord(state, parsed, payloadHash, options.leaseFence, executable, policy);
|
|
66
75
|
if (!executable) {
|
|
67
76
|
const saved = await state.journal.putOperation(scopeKey, operation);
|
|
68
77
|
return rejected(409, runtimeError('capability_unavailable', state.journal.destroyed() ? 'runtime_destroyed' : state.nativeError ?? 'adapter_not_started', { operationId: saved.operationId }));
|
|
@@ -8,7 +8,7 @@ const instances = new WeakMap();
|
|
|
8
8
|
const executing = new WeakMap();
|
|
9
9
|
const freezing = new WeakMap();
|
|
10
10
|
const TERMINAL = new Set(['completed', 'failed', 'interrupted']);
|
|
11
|
-
const EXCLUSIVE_OPERATIONS = new Set(['preview.publish', 'delivery.build', 'directory.create',
|
|
11
|
+
const EXCLUSIVE_OPERATIONS = new Set(['preview.publish', 'delivery.build', 'model.preview', 'directory.create',
|
|
12
12
|
'directory.rename', 'directory.delete', 'workspace.references.import', 'skills.update', 'parameter.verify',
|
|
13
13
|
'checkpoint.create', 'checkpoint.restore']);
|
|
14
14
|
export function managedDirectoryScope(state, value) {
|
|
@@ -152,7 +152,7 @@ async function gateFor(state) {
|
|
|
152
152
|
gate.restore({ ...terminal, owner: `terminal:${terminal.terminalId}`, mode: 'shared' });
|
|
153
153
|
for (const writer of state.journal.listManagedWrites())
|
|
154
154
|
if (!writer.stoppedAt) {
|
|
155
|
-
gate.restore({ ...managedDirectoryScope(state, writer.workspaceId), owner: `managed:${writer.writeId}`, mode: 'shared' });
|
|
155
|
+
gate.restore({ ...managedDirectoryScope(state, writer.workspaceId), owner: `managed:${writer.writeId}`, mode: writer.directoryAccessMode ?? 'shared' });
|
|
156
156
|
}
|
|
157
157
|
const waiting = [];
|
|
158
158
|
for (const barrier of state.journal.directoryBarriers()) {
|
|
@@ -237,6 +237,11 @@ export async function withDirectoryOwner(state, scope, owner, action) {
|
|
|
237
237
|
throw new Error('directory_owner_not_held');
|
|
238
238
|
return context.run({ state, claim: { ...scope, owner, mode: 'exclusive' } }, action);
|
|
239
239
|
}
|
|
240
|
+
/** Verify the existing claim without acquiring or upgrading a lease from a tool callback. */
|
|
241
|
+
export async function assertOperationDirectoryAccess(state, operation, mode) {
|
|
242
|
+
if (!(await gateFor(state)).permits(`operation:${operation.operationId}`, operation, mode))
|
|
243
|
+
throw new Error('requires-exclusive-turn');
|
|
244
|
+
}
|
|
240
245
|
export async function withDirectoryWrite(state, scope, action) {
|
|
241
246
|
const inherited = context.getStore();
|
|
242
247
|
const gate = await gateFor(state);
|
|
@@ -314,9 +319,11 @@ export async function withOperationDirectoryAccess(state, operation, action, def
|
|
|
314
319
|
const current = state.journal.getOperationById(operation.operationId);
|
|
315
320
|
if (!current || TERMINAL.has(current.status))
|
|
316
321
|
return;
|
|
317
|
-
|
|
318
|
-
|
|
322
|
+
const cancelled = current.status === 'queued' && error instanceof Error && error.message === 'directory_access_cancelled';
|
|
323
|
+
await state.journal.putOperation(deferred.scopeKey, transitionOperation(current, cancelled ? 'interrupted' : 'uncertain', {
|
|
324
|
+
error: cancelled ? 'cancelled' : 'native_receipt_lost',
|
|
319
325
|
}));
|
|
326
|
+
await state.journal.deleteDeferredDirectoryOperation(operation.operationId);
|
|
320
327
|
}).catch(() => undefined);
|
|
321
328
|
return state.journal.getOperationById(operation.operationId) ?? operation;
|
|
322
329
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { withObservedFile } from '../workspaces/revisions.js';
|
|
2
2
|
import { revisionScope, sessionWorkspaceRoot, workspaceRoot, workspaceSessions } from '../workspaces/registry.js';
|
|
3
|
+
import { preflightSkillExecution } from '../workspaces/skill-execution-context.js';
|
|
3
4
|
import { abandonTurnChanges, beginTurnChanges, finishTurnChanges } from '../workspaces/turn-changes.js';
|
|
4
5
|
import { shareWorkspaceGit } from '../workspaces/shared-git.js';
|
|
5
|
-
import { holdDirectoryAccess, withOperationDirectoryAccess } from './directory-access.js';
|
|
6
|
+
import { cancelPendingDirectoryAccess, holdDirectoryAccess, withOperationDirectoryAccess } from './directory-access.js';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import { z } from 'zod';
|
|
8
|
-
import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, directoryRenamePayloadSchema, directoryDeletePayloadSchema, directoryMutationReceiptSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, deliveryBuildPayloadSchema, deliveryCancelPayloadSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
|
+
import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, directoryRenamePayloadSchema, directoryDeletePayloadSchema, directoryMutationReceiptSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, deliveryBuildPayloadSchema, deliveryCancelPayloadSchema, workspaceModelPreviewBuildSchema, workspaceModelPreviewCancelSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
10
|
import { closeCliCredentials, ensureCliCredentials } from '../workspaces/cli-credentials.js';
|
|
10
11
|
import { artifactFormat } from '../workspaces/artifact-files.js';
|
|
11
12
|
import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
@@ -42,6 +43,7 @@ import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.j
|
|
|
42
43
|
import { publishAssetTransfer } from '../workspaces/asset-transfer-publish.js';
|
|
43
44
|
import { AssetTransferError } from '../workspaces/asset-transfer-storage.js';
|
|
44
45
|
import { runDeliveryBuild, cancelDeliveryBuild, trackDeliveryBuild, DeliveryBuildError } from '../workspaces/delivery-build.js';
|
|
46
|
+
import { runModelPreviewBuild, cancelModelPreview, modelPreviewError } from '../workspaces/procedural-3d/model-preview-build.js';
|
|
45
47
|
const CONVERSATION_KINDS = new Set([
|
|
46
48
|
'session.create',
|
|
47
49
|
'session.turn',
|
|
@@ -76,6 +78,8 @@ const LOCAL_KINDS = new Set([
|
|
|
76
78
|
'preview.publish',
|
|
77
79
|
'delivery.build',
|
|
78
80
|
'delivery.cancel',
|
|
81
|
+
'model.preview',
|
|
82
|
+
'model.preview.cancel',
|
|
79
83
|
'skills.update',
|
|
80
84
|
]);
|
|
81
85
|
const parameterVerifications = new WeakMap();
|
|
@@ -119,7 +123,7 @@ export async function executeOperation(state, scopeKey, operation, payload, reco
|
|
|
119
123
|
return executeScopedOperation(state, scopeKey, operation, payload, recovery);
|
|
120
124
|
}, { operationId: operation.operationId, scopeKey, payload, ...(recovery ? { recovery } : {}) });
|
|
121
125
|
};
|
|
122
|
-
if (['runtime.destroy', 'session.interrupt', 'session.approval.respond', 'session.steer', 'preview.publish', 'delivery.build', 'delivery.cancel'].includes(operation.kind)) {
|
|
126
|
+
if (['runtime.destroy', 'session.interrupt', 'session.approval.respond', 'session.steer', 'preview.publish', 'delivery.build', 'delivery.cancel', 'model.preview', 'model.preview.cancel'].includes(operation.kind)) {
|
|
123
127
|
return run();
|
|
124
128
|
}
|
|
125
129
|
return state.journal.exclusive(operation.kind.startsWith('session.') && operation.sessionId
|
|
@@ -201,6 +205,9 @@ async function executeScopedOperation(state, scopeKey, operation, payload, recov
|
|
|
201
205
|
if (operation.kind === 'delivery.cancel') {
|
|
202
206
|
return executeDeliveryCancel(state, scopeKey, current, payload);
|
|
203
207
|
}
|
|
208
|
+
if (operation.kind === 'model.preview' || operation.kind === 'model.preview.cancel') {
|
|
209
|
+
return executeModelPreview(state, scopeKey, current, payload);
|
|
210
|
+
}
|
|
204
211
|
if (!state.native?.ready()) {
|
|
205
212
|
return persist(state, scopeKey, transitionOperation(current, 'failed', { error: 'capability_unavailable' }));
|
|
206
213
|
}
|
|
@@ -411,6 +418,20 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
411
418
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
|
|
412
419
|
}
|
|
413
420
|
const root = await sessionWorkspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
421
|
+
try {
|
|
422
|
+
const policy = await preflightSkillExecution(state, { ...operation, payload });
|
|
423
|
+
if (policy?.directoryAccessMode === 'exclusive' && operation.directoryAccessMode !== 'exclusive')
|
|
424
|
+
throw new ComposerContextError('requires-exclusive-turn');
|
|
425
|
+
if (operation.skillExecutionPolicyVersion === 1 && (!policy ||
|
|
426
|
+
(parameterTask.data ? 'exclusive' : policy.directoryAccessMode) !== operation.directoryAccessMode))
|
|
427
|
+
throw new ComposerContextError('context_skills_changed');
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', {
|
|
431
|
+
error: error instanceof Error && /^(?:context_|requires-exclusive-turn)/.test(error.message)
|
|
432
|
+
? error.message : 'context_skill_unavailable',
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
414
435
|
let composer;
|
|
415
436
|
let initializationSkill;
|
|
416
437
|
try {
|
|
@@ -858,9 +879,32 @@ async function executeInterrupt(state, scopeKey, operation, payload) {
|
|
|
858
879
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unknown_session' }));
|
|
859
880
|
}
|
|
860
881
|
const expectedTurnId = asRecord(payload).expectedTurnId;
|
|
882
|
+
const cancelTarget = asRecord(payload).cancelQueuedOperationId;
|
|
861
883
|
if (expectedTurnId !== undefined && (typeof expectedTurnId !== 'string' || !expectedTurnId
|
|
862
884
|
|| binding.activeTurnId !== expectedTurnId))
|
|
863
885
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_changed' }));
|
|
886
|
+
// A directory-gated turn has no native turn to interrupt. Cancel only a proven pending
|
|
887
|
+
// claim; an admitted/racing claim still goes through the native interruption contract.
|
|
888
|
+
if (expectedTurnId === undefined && !binding.activeTurnId) {
|
|
889
|
+
const candidates = state.journal.deferredDirectoryOperations().flatMap(pending => {
|
|
890
|
+
const queued = state.journal.getOperationById(pending.operationId);
|
|
891
|
+
return queued?.status === 'queued' && ['session.turn', 'session.review', 'session.compact'].includes(queued.kind)
|
|
892
|
+
&& queued.sessionId === sessionId && queued.workspaceId === operation.workspaceId
|
|
893
|
+
&& (queued.directoryName ?? 'basic') === (operation.directoryName ?? 'basic')
|
|
894
|
+
&& (cancelTarget === undefined || cancelTarget === queued.operationId || cancelTarget === queued.gatewayOperationId)
|
|
895
|
+
&& !queued.nativeTurnId && !queued.nativeRequestId ? [{ pending, queued }] : [];
|
|
896
|
+
});
|
|
897
|
+
if (candidates.length === 1) {
|
|
898
|
+
const { pending, queued } = candidates[0];
|
|
899
|
+
if (await cancelPendingDirectoryAccess(state, `operation:${queued.operationId}`)) {
|
|
900
|
+
await persist(state, pending.scopeKey, transitionOperation(queued, 'interrupted', { error: 'cancelled' }));
|
|
901
|
+
await state.journal.deleteDeferredDirectoryOperation(queued.operationId);
|
|
902
|
+
return persist(state, scopeKey, transitionOperation(transitionOperation(operation, 'accepted'), 'completed'));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (cancelTarget !== undefined && expectedTurnId === undefined)
|
|
907
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_changed' }));
|
|
864
908
|
const result = await native(state).interrupt({ sessionId, threadId: binding.threadId,
|
|
865
909
|
...(typeof expectedTurnId === 'string' ? { expectedTurnId } : {}) });
|
|
866
910
|
if (result.outcome === 'failed') {
|
|
@@ -1466,6 +1510,29 @@ async function executePreviewPublish(state, scopeKey, operation) {
|
|
|
1466
1510
|
}).catch(() => undefined));
|
|
1467
1511
|
return accepted;
|
|
1468
1512
|
}
|
|
1513
|
+
async function executeModelPreview(state, scopeKey, operation, payload) {
|
|
1514
|
+
if (!operation.workspaceId || operation.directoryName === 'root')
|
|
1515
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'model_preview_scope_invalid' }));
|
|
1516
|
+
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
1517
|
+
if (operation.kind === 'model.preview.cancel') {
|
|
1518
|
+
try {
|
|
1519
|
+
const input = workspaceModelPreviewCancelSchema.parse(payload);
|
|
1520
|
+
await cancelModelPreview(state, operation.workspaceId, operation.directoryName ?? 'basic', input.previewId);
|
|
1521
|
+
return persist(state, scopeKey, transitionOperation(accepted, 'completed'));
|
|
1522
|
+
}
|
|
1523
|
+
catch (error) {
|
|
1524
|
+
return persist(state, scopeKey, transitionOperation(accepted, 'failed', { error: modelPreviewError(error) }));
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const parsed = workspaceModelPreviewBuildSchema.safeParse(payload);
|
|
1528
|
+
if (!parsed.success)
|
|
1529
|
+
return persist(state, scopeKey, transitionOperation(accepted, 'failed', { error: 'model_preview_invalid_request' }));
|
|
1530
|
+
trackPreviewPublish(state, runModelPreviewBuild(state, accepted, parsed.data).then(async () => { await persist(state, scopeKey, transitionOperation(accepted, 'completed')); }, async (error) => {
|
|
1531
|
+
const code = modelPreviewError(error);
|
|
1532
|
+
await persist(state, scopeKey, transitionOperation(accepted, code === 'model_preview_process_unknown' ? 'uncertain' : code === 'model_preview_cancelled' ? 'interrupted' : 'failed', { error: code }));
|
|
1533
|
+
}).catch(() => undefined));
|
|
1534
|
+
return accepted;
|
|
1535
|
+
}
|
|
1469
1536
|
async function executeDeliveryBuild(state, scopeKey, operation, payload) {
|
|
1470
1537
|
const parsed = deliveryBuildPayloadSchema.safeParse(payload);
|
|
1471
1538
|
if (!parsed.success) {
|
|
@@ -22,7 +22,7 @@ import { validateDeliveryArtifact, DeliveryValidationError } from '../workspaces
|
|
|
22
22
|
import { DeliveryStore } from '../workspaces/delivery-store.js';
|
|
23
23
|
import { importWorkspaceLibraryAttachment, listWorkspaceLibraryAttachments, listWorkspaceLibraryImportReceipts, readLibraryAttachmentUpload } from '../workspaces/library-attachments.js';
|
|
24
24
|
import { beginWorkspaceDirectoryAttachment, uploadWorkspaceDirectoryFile, completeWorkspaceDirectoryAttachment, cancelWorkspaceDirectoryAttachment } from '../workspaces/directory-attachments.js';
|
|
25
|
-
import { readWorkspaceSkills } from '../workspaces/skills.js';
|
|
25
|
+
import { readWorkspaceSkills, readWorkspaceSkillSnapshot } from '../workspaces/skills.js';
|
|
26
26
|
import { listWorkspaceRemixReferences, RemixReferenceError } from '../workspaces/remix-references.js';
|
|
27
27
|
import { loadWorkspaceSkillArtifact } from '../workspaces/skill-artifact-loader.js';
|
|
28
28
|
import { join } from 'node:path';
|
|
@@ -47,6 +47,8 @@ import { restoreNativeSession, SessionRestoreError } from './session-restore.js'
|
|
|
47
47
|
import { handleAssetTransferRequest } from './asset-transfers.js';
|
|
48
48
|
import { readWorktreeSource, registerChatWorktree } from '../workspaces/worktrees.js';
|
|
49
49
|
import { withDirectoryWrite } from './directory-access.js';
|
|
50
|
+
import { readProceduralModelCandidates } from '../workspaces/procedural-3d/read-candidates.js';
|
|
51
|
+
import { ModelPreviewStore } from '../workspaces/procedural-3d/model-preview-store.js';
|
|
50
52
|
export async function handleRuntimeRequest(state, request, response) {
|
|
51
53
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
52
54
|
const workspaceId = url.searchParams.get('workspaceId');
|
|
@@ -137,6 +139,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
137
139
|
...(state.native?.scheduledTaskProposalSource ? { scheduledTaskProposalVersion: 1 } : {}),
|
|
138
140
|
parameterAgentVersion: 1,
|
|
139
141
|
directoryConcurrencyVersion: 1,
|
|
142
|
+
skillExecutionPolicyVersion: 1,
|
|
140
143
|
parameterInitializationVersion: 1,
|
|
141
144
|
remixReferencesVersion: 1,
|
|
142
145
|
batchSourceFreezeRevision: 2,
|
|
@@ -530,6 +533,40 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
530
533
|
writeJson(response, 200, deliveryBuildDetailSchema.parse({ record, ...(report ? { report } : {}) }));
|
|
531
534
|
return;
|
|
532
535
|
}
|
|
536
|
+
if (['GET', 'HEAD'].includes(request.method ?? '') && url.pathname === '/runtime/model-preview-content') {
|
|
537
|
+
if (writeDestroyed(state, response))
|
|
538
|
+
return;
|
|
539
|
+
const workspaceId = url.searchParams.get('workspaceId');
|
|
540
|
+
if (!workspaceId || directoryName === 'root') {
|
|
541
|
+
writeJson(response, 400, { error: 'invalid_workspace' });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
545
|
+
try {
|
|
546
|
+
const output = await new ModelPreviewStore(state.config.stateDir, root).content(url.searchParams.get('previewId') ?? '');
|
|
547
|
+
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'content-length': output.bytes.length,
|
|
548
|
+
'cache-control': 'private, no-store', 'x-content-type-options': 'nosniff', 'x-content-sha256': output.record.contentHash,
|
|
549
|
+
'content-security-policy': "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'",
|
|
550
|
+
'referrer-policy': 'no-referrer' });
|
|
551
|
+
response.end(request.method === 'HEAD' ? undefined : output.bytes);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
writeJson(response, 404, { error: 'model_preview_unavailable' });
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (request.method === 'GET' && url.pathname === '/runtime/model-candidates') {
|
|
559
|
+
if (writeDestroyed(state, response))
|
|
560
|
+
return;
|
|
561
|
+
const workspaceId = url.searchParams.get('workspaceId');
|
|
562
|
+
if (!workspaceId || directoryName === 'root') {
|
|
563
|
+
writeJson(response, 400, { error: 'invalid_workspace' });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const root = await workspaceRoot(state, workspaceId, directoryName);
|
|
567
|
+
writeJson(response, 200, await readProceduralModelCandidates(root, state.config.stateDir));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
533
570
|
if (request.method === 'GET' && url.pathname === '/runtime/artifacts') {
|
|
534
571
|
if (writeDestroyed(state, response))
|
|
535
572
|
return;
|
|
@@ -707,6 +744,20 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
707
744
|
}
|
|
708
745
|
return;
|
|
709
746
|
}
|
|
747
|
+
if (request.method === 'GET' && url.pathname === '/runtime/skills/snapshot') {
|
|
748
|
+
if (writeDestroyed(state, response))
|
|
749
|
+
return;
|
|
750
|
+
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
751
|
+
try {
|
|
752
|
+
writeJson(response, 200, await readWorkspaceSkillSnapshot(await workspaceRoot(state, workspaceId, directoryName)));
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (error instanceof WorkspaceContextError)
|
|
756
|
+
throw error;
|
|
757
|
+
writeJson(response, 409, { error: 'context_skill_unavailable' });
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
710
761
|
if (request.method === 'GET' && url.pathname === '/runtime/skills') {
|
|
711
762
|
if (writeDestroyed(state, response))
|
|
712
763
|
return;
|
|
@@ -974,8 +1025,15 @@ async function acceptOperation(state, request, response) {
|
|
|
974
1025
|
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
975
1026
|
return;
|
|
976
1027
|
}
|
|
977
|
-
if (state.config.workspaceMode === 'multi' && parsed.data.kind === 'runtime.ensure' && parsed.data.workspaceId)
|
|
978
|
-
|
|
1028
|
+
if (state.config.workspaceMode === 'multi' && parsed.data.kind === 'runtime.ensure' && parsed.data.workspaceId && parsed.data.directoryName !== 'root') {
|
|
1029
|
+
const workspaceId = parsed.data.workspaceId, directoryName = parsed.data.directoryName ?? 'basic';
|
|
1030
|
+
const restoring = state.journal.getWorkspace(workspaceId)?.directories?.some(directory => directory.directoryName === directoryName && directory.status === 'reclaimed') ?? false;
|
|
1031
|
+
// Opening a resident checkout only refreshes instructions and can share its background install.
|
|
1032
|
+
// Rebuilding a reclaimed checkout must still exclude every existing writer.
|
|
1033
|
+
await withDirectoryWrite(state, { workspaceId, directoryName, mode: restoring ? 'exclusive' : 'shared', wait: false }, async () => {
|
|
1034
|
+
await restoreWorkspaceDirectory(state, workspaceId, directoryName, restoring);
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
979
1037
|
if (state.config.workspaceMode === 'multi')
|
|
980
1038
|
await sessionWorkspaceRoot(state, parsed.data.workspaceId, parsed.data.directoryName);
|
|
981
1039
|
// Shared classification: a new conversation writes no files, so a batch hold must not refuse it.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { workspaceComposerSelectionSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
3
|
/** One construction point for operation records so HTTP and in-sandbox callers cannot drift. */
|
|
3
|
-
export function buildOperationRecord(state, parsed, payloadHash, leaseFence, executable = true) {
|
|
4
|
+
export function buildOperationRecord(state, parsed, payloadHash, leaseFence, executable = true, policy) {
|
|
4
5
|
const now = new Date().toISOString();
|
|
5
6
|
const payload = parsed.payload && typeof parsed.payload === 'object' && !Array.isArray(parsed.payload)
|
|
6
7
|
? parsed.payload
|
|
@@ -8,6 +9,8 @@ export function buildOperationRecord(state, parsed, payloadHash, leaseFence, exe
|
|
|
8
9
|
return {
|
|
9
10
|
operationId: randomUUID(),
|
|
10
11
|
...(parsed.gatewayOperationId ? { gatewayOperationId: parsed.gatewayOperationId } : {}),
|
|
12
|
+
...(policy ? { skillExecutionPolicyVersion: policy.skillExecutionPolicyVersion, directoryAccessMode: policy.directoryAccessMode,
|
|
13
|
+
...workspaceComposerSelectionSchema.parse({ context: payload.context, workMode: payload.workMode }) } : {}),
|
|
11
14
|
...(parsed.kind === 'session.turn' && payload.parameterTask !== undefined ? { directoryAccessMode: 'exclusive' } : {}),
|
|
12
15
|
environment: state.config.environment,
|
|
13
16
|
userId: state.config.userId,
|
|
@@ -5,6 +5,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { launcherSocket, runDeveloperCommand } from './developer-command.js';
|
|
7
7
|
import { adoptDirectoryAccess, holdDirectoryAccess, managedDirectoryScope } from '../server/directory-access.js';
|
|
8
|
+
import { stopLauncherExecution } from '../codex/launcher-client.js';
|
|
8
9
|
/** Long enough for a cold store on a game template; a stuck registry must not hold the write lease. */
|
|
9
10
|
const INSTALL_TIMEOUT_MS = 10 * 60_000;
|
|
10
11
|
const LOG_TAIL_LINES = 50;
|
|
@@ -96,7 +97,7 @@ export async function ensureWorkspaceDependencies(state, workspaceId, root) {
|
|
|
96
97
|
* A lost controller makes the install result unknown. Keep its writer claim until a supervisor
|
|
97
98
|
* confirms that the process group stopped; a closed connection alone cannot prove that.
|
|
98
99
|
*/
|
|
99
|
-
export async function recoverInterruptedInstalls(journal) {
|
|
100
|
+
export async function recoverInterruptedInstalls(journal, recovery) {
|
|
100
101
|
const now = new Date().toISOString();
|
|
101
102
|
for (const record of journal.listWorkspaceDependencies()) {
|
|
102
103
|
if (record.status !== 'installing')
|
|
@@ -108,6 +109,26 @@ export async function recoverInterruptedInstalls(journal) {
|
|
|
108
109
|
finishedAt: now,
|
|
109
110
|
});
|
|
110
111
|
}
|
|
112
|
+
for (const writer of journal.listManagedWrites()) {
|
|
113
|
+
if (writer.stoppedAt || writer.source !== 'background' || writer.executionMode === 'local' || !writer.writeId.startsWith(WRITE_PREFIX))
|
|
114
|
+
continue;
|
|
115
|
+
// As with terminals, the trusted supervisor advances an epoch only after stop-all is confirmed.
|
|
116
|
+
const stoppedEpoch = recovery?.launcherSocket && recovery.identityMatches && recovery.previousRuntimeEpoch !== undefined &&
|
|
117
|
+
recovery.runtimeEpoch > recovery.previousRuntimeEpoch &&
|
|
118
|
+
(writer.runtimeEpoch ?? recovery.previousRuntimeEpoch) < recovery.runtimeEpoch;
|
|
119
|
+
if (!stoppedEpoch) {
|
|
120
|
+
if (!recovery?.identityMatches || !recovery.launcherSocket || !writer.launcher ||
|
|
121
|
+
writer.launcher.processId !== writer.writeId || writer.launcher.runtimeEpoch !== writer.runtimeEpoch)
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
await stopLauncherExecution(recovery.launcherSocket, writer.launcher);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await journal.putManagedWrite({ ...writer, stoppedAt: now });
|
|
131
|
+
}
|
|
111
132
|
}
|
|
112
133
|
async function installOnce(state, workspaceId, root, writeId) {
|
|
113
134
|
const startedAt = new Date().toISOString();
|
|
@@ -150,7 +171,9 @@ async function installOnce(state, workspaceId, root, writeId) {
|
|
|
150
171
|
}
|
|
151
172
|
await excludeFromGit(root);
|
|
152
173
|
// A checkpoint waits for this write: node_modules is half written until the install ends.
|
|
153
|
-
|
|
174
|
+
const writer = { writeId, workspaceId, source: 'background', startedAt, runtimeEpoch: state.runtimeEpoch,
|
|
175
|
+
executionMode: launcherSocket(state) ? 'launcher' : 'local' };
|
|
176
|
+
await state.journal.putManagedWrite(writer);
|
|
154
177
|
await state.journal.putWorkspaceDependencies({
|
|
155
178
|
workspaceId,
|
|
156
179
|
status: 'installing',
|
|
@@ -163,7 +186,7 @@ async function installOnce(state, workspaceId, root, writeId) {
|
|
|
163
186
|
const shared = !launcherSocket(state) || (await waitUntilShared(root));
|
|
164
187
|
if (shared) {
|
|
165
188
|
processMayRun = true;
|
|
166
|
-
outcome = await runInstall(state,
|
|
189
|
+
outcome = await runInstall(state, root, writer);
|
|
167
190
|
processStopped = outcome.code !== undefined;
|
|
168
191
|
}
|
|
169
192
|
else
|
|
@@ -172,10 +195,7 @@ async function installOnce(state, workspaceId, root, writeId) {
|
|
|
172
195
|
finally {
|
|
173
196
|
if (!processMayRun || processStopped)
|
|
174
197
|
await state.journal.putManagedWrite({
|
|
175
|
-
writeId,
|
|
176
|
-
workspaceId,
|
|
177
|
-
source: 'background',
|
|
178
|
-
startedAt,
|
|
198
|
+
...(state.journal.getManagedWrite(writeId) ?? writer),
|
|
179
199
|
stoppedAt: new Date().toISOString(),
|
|
180
200
|
});
|
|
181
201
|
}
|
|
@@ -203,22 +223,27 @@ async function installOnce(state, workspaceId, root, writeId) {
|
|
|
203
223
|
* Runs as the development user through the launcher, so the packages belong to the same user as
|
|
204
224
|
* Codex and its own later installs. Without a launcher (local runs and tests) it runs directly.
|
|
205
225
|
*/
|
|
206
|
-
async function runInstall(state,
|
|
226
|
+
async function runInstall(state, root, writer) {
|
|
207
227
|
// Tests replace the install; a real registry is never reached from them.
|
|
208
228
|
const planned = process.env.PLAYCRAFT_DEPENDENCY_INSTALL_COMMAND || dependencyInstallCommand(state);
|
|
209
229
|
const logDir = join(state.config.stateDir, 'dependency-logs');
|
|
210
230
|
// private
|
|
211
231
|
await mkdir(logDir, { recursive: true, mode: 0o700 });
|
|
212
|
-
const logPath = join(logDir, `${workspaceId}.log`);
|
|
232
|
+
const logPath = join(logDir, `${writer.workspaceId}.log`);
|
|
213
233
|
// private
|
|
214
234
|
await writeFile(logPath, `$ ${planned}\n`, { mode: 0o600 });
|
|
215
235
|
let log = '';
|
|
216
236
|
let pending = Promise.resolve();
|
|
217
237
|
const outcome = await runDeveloperCommand(state, {
|
|
218
|
-
processId:
|
|
238
|
+
processId: writer.writeId,
|
|
219
239
|
cwd: root,
|
|
220
240
|
command: planned,
|
|
221
241
|
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
242
|
+
onStarted: async (identity) => {
|
|
243
|
+
const current = state.journal.getManagedWrite(writer.writeId) ?? writer;
|
|
244
|
+
if (!current.stoppedAt)
|
|
245
|
+
await state.journal.putManagedWrite({ ...current, ...identity });
|
|
246
|
+
},
|
|
222
247
|
onOutput: (chunk) => {
|
|
223
248
|
const text = String(chunk);
|
|
224
249
|
log = (log + text).slice(-64 * 1024);
|
|
@@ -13,97 +13,98 @@ export function launcherSocket(state) {
|
|
|
13
13
|
export async function runDeveloperCommand(state, input) {
|
|
14
14
|
if (input.signal?.aborted)
|
|
15
15
|
return { code: null, timedOut: false, canceled: true };
|
|
16
|
-
const
|
|
16
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
17
|
+
const command = `{ umask 007; ${input.command}; } </dev/null 2>&1`;
|
|
18
|
+
const gate = input.onStarted ? 'IFS= read -r playcraft_start && [ "$playcraft_start" = start ] && ' : '';
|
|
19
|
+
const args = ['-c', `umask 007; ${gate}exec "$BASH" -lc "$1"`, 'playcraft-command', command];
|
|
17
20
|
const socketPath = launcherSocket(state);
|
|
18
21
|
const launched = socketPath
|
|
19
22
|
? await startLauncherProcess({
|
|
20
23
|
socketPath,
|
|
21
24
|
processId: input.processId,
|
|
22
25
|
runtimeEpoch: state.runtimeEpoch,
|
|
23
|
-
argv: ['/usr/bin/bash',
|
|
26
|
+
argv: ['/usr/bin/bash', ...args],
|
|
24
27
|
cwd: input.cwd,
|
|
25
28
|
env: {},
|
|
26
29
|
})
|
|
27
30
|
: undefined;
|
|
28
|
-
launched?.stdin?.end();
|
|
29
31
|
const local = launched
|
|
30
32
|
? undefined
|
|
31
|
-
: spawn('bash',
|
|
33
|
+
: spawn('bash', args, {
|
|
32
34
|
cwd: input.cwd,
|
|
33
35
|
detached: true,
|
|
34
|
-
stdio: ['
|
|
36
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
37
|
});
|
|
36
38
|
const stdout = launched?.stdout ?? local.stdout;
|
|
37
39
|
const stderr = launched?.stderr ?? local.stderr;
|
|
40
|
+
local?.stdin.on('error', () => undefined);
|
|
38
41
|
stdout.on('data', input.onOutput);
|
|
39
42
|
stderr.on('data', input.onOutput);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (launched) {
|
|
48
|
-
void launched.stop().catch(() => undefined);
|
|
49
|
-
return;
|
|
43
|
+
const exited = new Promise(resolve => {
|
|
44
|
+
const finish = (code) => resolve({ kind: 'exit', code });
|
|
45
|
+
if (launched)
|
|
46
|
+
launched.onExit(finish);
|
|
47
|
+
else {
|
|
48
|
+
local.once('error', () => finish(null));
|
|
49
|
+
local.once('close', finish);
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
});
|
|
52
|
+
let stopping;
|
|
53
|
+
const stop = () => stopping ??= (async () => {
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
+
if (launched) {
|
|
56
|
+
await launched.stop();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return !local?.pid || await stopBackgroundWriter(local.pid);
|
|
55
60
|
}
|
|
56
61
|
catch {
|
|
57
|
-
|
|
62
|
+
return false;
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
64
|
+
})();
|
|
65
|
+
let timedOut = false;
|
|
66
|
+
let canceled = false;
|
|
67
|
+
let interrupt;
|
|
68
|
+
const interrupted = new Promise(resolve => {
|
|
69
|
+
interrupt = () => { resolve({ kind: 'interrupted' }); void stop(); };
|
|
70
|
+
});
|
|
71
|
+
const abort = () => { canceled = true; interrupt(); };
|
|
72
|
+
const timeout = () => { timedOut = true; interrupt(); };
|
|
69
73
|
input.signal?.addEventListener('abort', abort, { once: true });
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
process.kill(-local.pid, 'SIGKILL');
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
/* already gone */
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}, input.timeoutMs);
|
|
84
|
-
const finish = (value) => {
|
|
85
|
-
clearTimeout(timer);
|
|
86
|
-
if (killTimer)
|
|
87
|
-
clearTimeout(killTimer);
|
|
88
|
-
input.signal?.removeEventListener('abort', abort);
|
|
89
|
-
resolve(value);
|
|
90
|
-
};
|
|
91
|
-
if (launched)
|
|
92
|
-
launched.onExit(finish);
|
|
93
|
-
else {
|
|
94
|
-
local.once('error', () => finish(null));
|
|
95
|
-
local.once('close', (value) => finish(value));
|
|
96
|
-
}
|
|
74
|
+
const timer = setTimeout(timeout, Math.max(0, deadline - Date.now()));
|
|
75
|
+
if (input.signal?.aborted)
|
|
76
|
+
abort();
|
|
77
|
+
if (Date.now() >= deadline)
|
|
78
|
+
timeout();
|
|
79
|
+
const outcome = async (code) => ({
|
|
80
|
+
code: await stop() ? code : undefined, timedOut, ...(canceled ? { canceled: true } : {}),
|
|
97
81
|
});
|
|
98
|
-
let stopped = !launched && !local?.pid;
|
|
99
82
|
try {
|
|
100
|
-
if (
|
|
101
|
-
await
|
|
102
|
-
|
|
83
|
+
if (canceled || timedOut)
|
|
84
|
+
return await outcome(null);
|
|
85
|
+
if (input.onStarted) {
|
|
86
|
+
if (launched && !launched.executionIdentity)
|
|
87
|
+
throw new Error('launcher_execution_identity_unavailable');
|
|
88
|
+
const persisted = Promise.resolve().then(() => input.onStarted({ pid: launched?.pid ?? local?.pid,
|
|
89
|
+
...(launched ? { launcher: launched.executionIdentity } : {}) })).then(() => ({ kind: 'persisted' }));
|
|
90
|
+
const ready = await Promise.race([persisted, interrupted, exited]);
|
|
91
|
+
if (ready.kind !== 'persisted')
|
|
92
|
+
return await outcome(ready.kind === 'exit' ? ready.code : null);
|
|
103
93
|
}
|
|
104
|
-
|
|
105
|
-
|
|
94
|
+
// A late persistence result must never reopen a canceled or timed-out command gate.
|
|
95
|
+
if (canceled || timedOut)
|
|
96
|
+
return await outcome(null);
|
|
97
|
+
(launched?.stdin ?? local.stdin)?.end(input.onStarted ? 'start\n' : undefined);
|
|
98
|
+
const result = await Promise.race([exited, interrupted]);
|
|
99
|
+
return await outcome(result.kind === 'exit' ? result.code : null);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// The gate never opened. Retain the journal claim if this stop cannot be confirmed.
|
|
103
|
+
await stop();
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
input.signal?.removeEventListener('abort', abort);
|
|
106
109
|
}
|
|
107
|
-
catch { /* Transport loss cannot prove the process group has stopped. */ }
|
|
108
|
-
return { code: stopped ? code : undefined, timedOut, ...(canceled ? { canceled: true } : {}) };
|
|
109
110
|
}
|
|
@@ -94,7 +94,7 @@ export async function registerWorkspaceDirectory(state, input, leaseFence, sourc
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
/** A reclaimed checkout recovers its existing remote branch; no new variant or moving basic head is used. */
|
|
97
|
-
export async function restoreWorkspaceDirectory(state, workspaceId, directoryName = 'basic') {
|
|
97
|
+
export async function restoreWorkspaceDirectory(state, workspaceId, directoryName = 'basic', allowReclaimedRestore = true) {
|
|
98
98
|
if (directoryName === 'root')
|
|
99
99
|
return;
|
|
100
100
|
await state.journal.exclusive(`directory:${workspaceId}:${directoryName}`, async () => {
|
|
@@ -106,6 +106,9 @@ export async function restoreWorkspaceDirectory(state, workspaceId, directoryNam
|
|
|
106
106
|
await ensureWorkspaceArtifactInstructions(await workspaceRoot(state, workspaceId, directoryName));
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
|
+
// Recheck under the journal lock: a resident-directory caller holds only shared access.
|
|
110
|
+
if (!allowReclaimedRestore)
|
|
111
|
+
throw new Error('directory_busy');
|
|
109
112
|
if (binding.targetKind === 'standalone' || !state.gitCredentials)
|
|
110
113
|
throw new WorkspaceContextError('workspace_recovery_required');
|
|
111
114
|
const preparation = await state.gitCredentials.prepareDirectory(workspaceId, directoryName);
|
|
@@ -4,6 +4,7 @@ import { resolveCliPackageRoot } from '../../scaffold/compose.js';
|
|
|
4
4
|
import { importBuiltinProjectSkills } from '../../project-skills/builtin-import.js';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { canonicalProjectSkillsJson } from '@playcraft/common/project-skills';
|
|
7
|
+
import { WORKSPACE_BUILTIN_SKILLS } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
8
|
import { syncLocalProjectSkills } from '../../project-skills/local-store.js';
|
|
8
9
|
import { selectOptionalPlatformSkills } from './optional-platform-skills.js';
|
|
9
10
|
import { WORKSPACE_REMIX_REFERENCES_SKILL } from './remix-reference-skill.js';
|
|
@@ -16,7 +17,8 @@ export const EMPTY_WORKSPACE_REPOSITORY_SKILLS = [
|
|
|
16
17
|
WORKSPACE_REMIX_REFERENCES_SKILL,
|
|
17
18
|
'playcraft-build-optimizer',
|
|
18
19
|
];
|
|
19
|
-
export const EMPTY_WORKSPACE_PLATFORM_SKILLS =
|
|
20
|
+
export const EMPTY_WORKSPACE_PLATFORM_SKILLS = WORKSPACE_BUILTIN_SKILLS
|
|
21
|
+
.filter(skill => skill.defaultOnInitialize).map(skill => skill.name);
|
|
20
22
|
export const EMPTY_WORKSPACE_DEFAULT_SKILLS = [...EMPTY_WORKSPACE_REPOSITORY_SKILLS, ...EMPTY_WORKSPACE_PLATFORM_SKILLS];
|
|
21
23
|
/** Only called in a new, unpublished staging directory; existing workspaces are never scaffolded. */
|
|
22
24
|
export async function scaffoldEmptyWorkspace(root, skillCache, access) {
|