@playcraft/cli 0.1.43-beta → 0.1.45-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.
@@ -0,0 +1,159 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, mkdir, open, readdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, join, relative } from 'node:path';
5
+ import { gzip, gunzip } from 'node:zlib';
6
+ import { promisify } from 'node:util';
7
+ import { excludeFromGit } from '../workspaces/dependencies.js';
8
+ import { gitRead, gitWrite } from './git.js';
9
+ const compress = promisify(gzip);
10
+ const decompress = promisify(gunzip);
11
+ const privateNames = new Set(['.git', 'node_modules', '.ssh', '.aws', '.azure', '.gnupg', '.codex', '.npm', '.pnpm-store',
12
+ '.config', '.npmrc', '.yarnrc', '.yarnrc.yml', '.netrc', '.git-credentials', 'credentials.json', 'credentials', 'secrets',
13
+ 'id_rsa', 'id_ed25519', 'settings.local.json', '.playcraft.json', 'playcraft.config.json', '.playcraft-temp', '.playcraft-preview-out']);
14
+ function included(path) {
15
+ return !path.split('/').some(name => privateNames.has(name) || /^\.env(?:$|\.)/.test(name) || /\.(?:pem|key|p12|pfx)$/.test(name))
16
+ && !path.startsWith('.playcraft/skills/');
17
+ }
18
+ /** Git keeps only the executable bit, so the digest must not depend on umask or group-write bits. */
19
+ function snapshotMode(mode) {
20
+ return mode & 0o111 ? 0o755 : 0o644;
21
+ }
22
+ function inside(root, path) {
23
+ const rel = relative(root, path);
24
+ return rel !== '..' && !rel.startsWith('../') && !isAbsolute(rel);
25
+ }
26
+ export function digestFiles(files) {
27
+ const hash = createHash('sha256');
28
+ for (const [path, file] of files)
29
+ hash.update(path).update('\0').update(String(file.mode)).update('\0').update(file.body).update('\0');
30
+ return hash.digest('hex');
31
+ }
32
+ /** Working tree plus exact skill artifacts. Credentials, caches and node_modules are left out. */
33
+ export async function readSourceFiles(root) {
34
+ const source = await realpath(root);
35
+ const paths = new Set((await gitRead(source, ['ls-files', '--cached', '--others', '--exclude-standard', '-z'])).split('\0').filter(Boolean));
36
+ const artifacts = await readdir(join(source, '.playcraft/skill-artifacts')).catch(error => {
37
+ if (error.code === 'ENOENT')
38
+ return [];
39
+ throw error;
40
+ });
41
+ for (const name of artifacts)
42
+ if (/^[a-f0-9]{64}\.tar\.gz$/.test(name))
43
+ paths.add(`.playcraft/skill-artifacts/${name}`);
44
+ const files = new Map();
45
+ for (const path of [...paths].sort()) {
46
+ if (!included(path))
47
+ continue;
48
+ const full = join(source, path);
49
+ const stat = await lstat(full).catch(error => {
50
+ if (error.code === 'ENOENT')
51
+ return undefined;
52
+ throw error;
53
+ });
54
+ if (!stat)
55
+ continue;
56
+ if (!stat.isFile() || stat.isSymbolicLink() || !inside(source, await realpath(full)))
57
+ throw new Error('batch_source_path_invalid');
58
+ const handle = await open(full, constants.O_RDONLY | constants.O_NOFOLLOW);
59
+ try {
60
+ const body = await handle.readFile();
61
+ if (/^\.playcraft\/skill-artifacts\/[a-f0-9]{64}\.tar\.gz$/.test(path)
62
+ && createHash('sha256').update(body).digest('hex') !== path.slice(path.lastIndexOf('/') + 1, -7)) {
63
+ throw new Error('batch_source_path_invalid');
64
+ }
65
+ files.set(path, { body, mode: snapshotMode(stat.mode) });
66
+ }
67
+ finally {
68
+ await handle.close();
69
+ }
70
+ }
71
+ return files;
72
+ }
73
+ /**
74
+ * Drops paths whose bytes change between two reads. A managed writer that could not be stopped
75
+ * does not become part of the frozen input.
76
+ */
77
+ export async function filterUnstableFiles(root, files) {
78
+ const again = await readSourceFiles(root);
79
+ const stable = new Map();
80
+ for (const [path, file] of files) {
81
+ const next = again.get(path);
82
+ if (next && next.body.equals(file.body) && next.mode === file.mode)
83
+ stable.set(path, file);
84
+ }
85
+ return stable;
86
+ }
87
+ /** Independent root commit. The source branch, index and config are never written. */
88
+ export async function captureIndependentSource(source, destination) {
89
+ const root = await realpath(source);
90
+ if (await lstat(destination).catch(() => undefined))
91
+ throw new Error('batch_source_destination_exists');
92
+ const headBefore = await gitRead(root, ['rev-parse', '--verify', 'HEAD']).catch(() => '');
93
+ let files = await readSourceFiles(root);
94
+ files = await filterUnstableFiles(root, files);
95
+ const digest = digestFiles(files);
96
+ const staging = join(dirname(destination), `.batch-source-${randomUUID()}`);
97
+ try {
98
+ await mkdir(staging, { recursive: true });
99
+ await gitWrite(staging, ['init', '--initial-branch', 'source']);
100
+ await gitWrite(staging, ['config', 'user.name', 'PlayCraft']);
101
+ await gitWrite(staging, ['config', 'user.email', 'playcraft@localhost']);
102
+ for (const [path, file] of files) {
103
+ const full = join(staging, path);
104
+ await mkdir(dirname(full), { recursive: true });
105
+ await writeFile(full, file.body, { mode: file.mode, flag: 'wx' });
106
+ }
107
+ if (headBefore !== await gitRead(root, ['rev-parse', '--verify', 'HEAD']).catch(() => ''))
108
+ throw new Error('batch_source_changed');
109
+ const after = await filterUnstableFiles(root, await readSourceFiles(root));
110
+ if (digestFiles(after) !== digest)
111
+ throw new Error('batch_source_changed');
112
+ await excludeFromGit(staging);
113
+ await excludeFromGit(staging, '/.env*');
114
+ for (const entry of ['/.playcraft.json', '/playcraft.config.json', '/.playcraft/skills/', '/.playcraft-temp/'])
115
+ await excludeFromGit(staging, entry);
116
+ await gitWrite(staging, ['add', '-A', '.']);
117
+ await gitWrite(staging, ['commit', '--allow-empty', '-m', 'Freeze batch source']);
118
+ const commitHash = (await gitWrite(staging, ['rev-parse', 'HEAD'])).trim();
119
+ await rename(staging, destination);
120
+ return { commitHash, digest };
121
+ }
122
+ finally {
123
+ await rm(staging, { recursive: true, force: true });
124
+ }
125
+ }
126
+ export async function archiveSource(root, meta) {
127
+ const bundle = join(dirname(root), `.bundle-${randomUUID()}`);
128
+ try {
129
+ await gitWrite(root, ['bundle', 'create', bundle, 'HEAD']);
130
+ const body = JSON.stringify({ version: 1, ...meta, bundle: (await readFile(bundle)).toString('base64') });
131
+ return await compress(Buffer.from(body));
132
+ }
133
+ finally {
134
+ await rm(bundle, { force: true });
135
+ }
136
+ }
137
+ export async function restoreArchive(body, destination, expectedDigest) {
138
+ const archive = JSON.parse((await decompress(body, { maxOutputLength: 512 * 1024 * 1024 })).toString());
139
+ if (archive.version !== 1 || archive.digest !== expectedDigest || !archive.commitHash || !archive.bundle)
140
+ throw new Error('batch_snapshot_mismatch');
141
+ const temporary = join(dirname(destination), `.bundle-${randomUUID()}`);
142
+ await mkdir(temporary, { recursive: true });
143
+ try {
144
+ const bundle = join(temporary, 'source.bundle');
145
+ await writeFile(bundle, Buffer.from(archive.bundle, 'base64'), { mode: 0o600 });
146
+ await gitWrite(temporary, ['clone', '--no-checkout', '--', bundle, destination]);
147
+ await gitWrite(destination, ['checkout', '--detach', archive.commitHash]);
148
+ if (digestFiles(await readSourceFiles(destination)) !== expectedDigest)
149
+ throw new Error('batch_snapshot_mismatch');
150
+ return { commitHash: archive.commitHash };
151
+ }
152
+ catch (error) {
153
+ await rm(destination, { recursive: true, force: true });
154
+ throw error;
155
+ }
156
+ finally {
157
+ await rm(temporary, { recursive: true, force: true });
158
+ }
159
+ }
@@ -4,7 +4,7 @@ import { startLauncherProcess } from './launcher-client.js';
4
4
  import { chmod, mkdir, mkdtemp, rm } from 'node:fs/promises';
5
5
  import { relative, join } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
- import { APPROVAL_METHODS, executionPolicy, nativeApprovalResponse, NATIVE_APPROVALS_REVIEWER, threadExecutionParams } from './permissions.js';
7
+ import { APPROVAL_METHODS, executionPolicy, nativeApprovalResponse, NATIVE_APPROVALS_REVIEWER, resolveSessionExecution, sessionCanEscalate, threadExecutionParams } from './permissions.js';
8
8
  import { workspaceApprovalResponseSchema } from '@playcraft/common/cloud-workspace/runtime';
9
9
  import { portableCodexConfig, writePortableCodexCatalog } from './config-toml.js';
10
10
  import { createJsonRpcStdioClient, NativeRpcResponseError } from './jsonrpc-stdio.js';
@@ -212,7 +212,7 @@ export function createCodexAppServerAdapter(options) {
212
212
  if (await readProjectSkillsLockFile(cwd) === null) {
213
213
  if (options.config.workspaceMode === 'multi')
214
214
  throw new NativePreparationError('workspace_skills_unavailable');
215
- const execution = executionPolicy(cwd, permissionMode, options.fixedExecution);
215
+ const execution = resolveSessionExecution(cwd, permissionMode, sessionId ? options.trustedExecution?.(sessionId) : undefined, options.fixedExecution);
216
216
  const selected = threadExecutionParams(execution);
217
217
  return { params: { ...modelConfig, ...selected, config: { ...modelConfig.config, ...selected.config },
218
218
  ...(options.workspaceUi && !options.fixedExecution ? { developerInstructions: WORKSPACE_UI_INSTRUCTIONS } : {}) },
@@ -239,7 +239,7 @@ export function createCodexAppServerAdapter(options) {
239
239
  ...(options.workspaceUi && !options.fixedExecution ? [WORKSPACE_UI_INSTRUCTIONS] : []),
240
240
  ].join('\n');
241
241
  const discoveryHash = createHash('sha256').update(JSON.stringify(listing.skills.map(skill => skill.path).sort())).digest('hex');
242
- const execution = executionPolicy(cwd, permissionMode, options.fixedExecution);
242
+ const execution = resolveSessionExecution(cwd, permissionMode, sessionId ? options.trustedExecution?.(sessionId) : undefined, options.fixedExecution);
243
243
  const selected = threadExecutionParams(execution);
244
244
  return { params: { ...modelConfig, ...selected, config: { ...config, ...selected.config }, developerInstructions },
245
245
  execution, modelConfigId, activate, skillsHash: frozen.lockHash, discoveryHash, mcpHash: mcp?.hash };
@@ -678,7 +678,7 @@ export function createCodexAppServerAdapter(options) {
678
678
  throw new Error('native_receipt_lost');
679
679
  prepared.activate();
680
680
  threads.set(threadId, { sessionId: input.sessionId, cwd: input.cwd, skillsHash: prepared.skillsHash,
681
- discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: prepared.execution.approvalPolicy === 'on-request' || (!options.fixedExecution && prepared.execution.sandboxPolicy.type === 'dangerFullAccess'), hasNativeHistory: false, loaded: true });
681
+ discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: false, loaded: true });
682
682
  threads.get(threadId).readOnlyConfirmed = isReadOnlySnapshot(result);
683
683
  return { threadId, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto' };
684
684
  });
@@ -705,7 +705,7 @@ export function createCodexAppServerAdapter(options) {
705
705
  return undefined;
706
706
  prepared.activate();
707
707
  threads.set(input.threadId, { sessionId: input.sessionId, cwd: input.cwd, skillsHash: prepared.skillsHash,
708
- discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: prepared.execution.approvalPolicy === 'on-request' || (!options.fixedExecution && prepared.execution.sandboxPolicy.type === 'dangerFullAccess'), hasNativeHistory: true, loaded: true });
708
+ discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: true, loaded: true });
709
709
  threads.get(input.threadId).readOnlyConfirmed = readOnlyConfirmed;
710
710
  return { modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto' };
711
711
  });
@@ -730,7 +730,7 @@ export function createCodexAppServerAdapter(options) {
730
730
  const permissionMode = input.permissionMode ?? thread.permissionMode ?? 'auto';
731
731
  const workMode = input.workMode ?? 'execute';
732
732
  let activateModel = () => { };
733
- let execution = executionPolicy(thread.cwd ?? options.config.workspaceRoot, permissionMode, options.fixedExecution);
733
+ let execution = resolveSessionExecution(thread.cwd ?? options.config.workspaceRoot, permissionMode, options.trustedExecution?.(input.sessionId), options.fixedExecution);
734
734
  if (thread.cwd) {
735
735
  try {
736
736
  const prepared = await lockedThreadConfig(thread.cwd, input.modelConfigId ?? thread.modelConfigId, input.sessionId, reasoningEffort, input.clientRequestId, permissionMode);
@@ -774,7 +774,7 @@ export function createCodexAppServerAdapter(options) {
774
774
  thread.readOnlyConfirmationFailed = false;
775
775
  if (execution.sandboxPolicy.type !== 'readOnly' || execution.approvalPolicy !== 'never')
776
776
  thread.readOnlyConfirmed = false;
777
- thread.canEscalate = execution.approvalPolicy === 'on-request' || (!options.fixedExecution && execution.sandboxPolicy.type === 'dangerFullAccess');
777
+ thread.canEscalate = sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), execution, Boolean(options.fixedExecution));
778
778
  if (input.simulateLostReceipt)
779
779
  return { outcome: 'uncertain', threadId: input.threadId };
780
780
  try {
@@ -1,5 +1,14 @@
1
1
  export const NATIVE_APPROVALS_REVIEWER = 'user';
2
- /** Fixed production/coordinator boundaries may only become narrower. */
2
+ /** Trusted batch policy wins over the adapter-wide fixed policy. A browser mode may only narrow it. */
3
+ export function resolveSessionExecution(cwd, mode, trusted, fixed) {
4
+ return executionPolicy(cwd, mode, trusted ?? fixed);
5
+ }
6
+ /** A trusted unattended session cannot be escalated by a browser approval. */
7
+ export function sessionCanEscalate(trusted, execution, adapterHasFixedExecution) {
8
+ if (trusted)
9
+ return false;
10
+ return execution.approvalPolicy === 'on-request' || (!adapterHasFixedExecution && execution.sandboxPolicy.type === 'dangerFullAccess');
11
+ }
3
12
  export function executionPolicy(cwd, mode, fixed) {
4
13
  const directory = fixed?.cwd ?? cwd;
5
14
  if (mode === 'read-only')
@@ -8,6 +8,7 @@ import { productionExecutionRequestSchema } from './contracts.js';
8
8
  import { executeProductionStage } from './stages.js';
9
9
  import { groupExited, stopProductionGroup } from './processes.js';
10
10
  import { workspaceDirectoryOccupied } from '../workspaces/directory-maintenance.js';
11
+ import { batchDirectoryHeld } from '../batch/occupancy.js';
11
12
  import { settleDependencyInstalls } from '../workspaces/dependencies.js';
12
13
  import { verifyProductionLease } from './storage.js';
13
14
  const execute = promisify(execFile);
@@ -56,6 +57,8 @@ export async function productionDirectoryBusy(state, workspaceId, directoryName
56
57
  && !attempt.stopped && attempt.workspaceId === workspaceId && attempt.directoryName === directoryName)
57
58
  return true;
58
59
  }
60
+ if (await batchDirectoryHeld(state, workspaceId, directoryName))
61
+ return true;
59
62
  return false;
60
63
  }
61
64
  export async function executeSandboxProduction(state, raw) {
@@ -5,6 +5,7 @@ import { createServer } from 'node:http';
5
5
  import { INCOMPATIBLE_EXIT_CODE, negotiateProtocol, transitionOperation, } from '@playcraft/common/cloud-workspace/runtime';
6
6
  import { RuntimeJournal } from './persistence/journal.js';
7
7
  import { createCodexAppServerAdapter } from './codex/app-server.js';
8
+ import { trustedMemberPolicy } from './batch/policy.js';
8
9
  import { createLoopbackNativeAdapter } from './codex/loopback.js';
9
10
  import { attachNativeCollector } from './server/dispatch.js';
10
11
  import { handleRuntimeRequest } from './server/http.js';
@@ -275,6 +276,7 @@ function defaultNativeAdapter(state) {
275
276
  return loadWorkspaceSkillArtifact(state, workspaceId, entry, directoryName === 'root' ? 'basic' : directoryName);
276
277
  },
277
278
  onSkillProcessRestart: process => state.journal.noteExecutionGroup(process),
279
+ trustedExecution: sessionId => trustedMemberPolicy(state, sessionId),
278
280
  });
279
281
  }
280
282
  return undefined;
@@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto';
2
2
  import { z } from 'zod';
3
3
  import { PLATFORM_ROUTES, modelConnectivityPath, platformEndpoint, } from '@playcraft/common/cloud-workspace/runtime';
4
4
  const PROBE_TIMEOUT_MS = 4_000;
5
+ // Issuance performs current authority reads and signing, rather than an endpoint echo.
6
+ const CREDENTIAL_TIMEOUT_MS = 10_000;
5
7
  const CACHE_MS = 15_000;
6
8
  const CREDENTIAL_RENEW_BEFORE_MS = 30_000;
7
9
  const MAX_RESPONSE_BYTES = 16 * 1024;
@@ -59,7 +61,7 @@ async function responseJson(response) {
59
61
  }
60
62
  }
61
63
  async function requestProbe(target, init, credentials = false) {
62
- const signal = AbortSignal.timeout(PROBE_TIMEOUT_MS);
64
+ const signal = AbortSignal.timeout(credentials ? CREDENTIAL_TIMEOUT_MS : PROBE_TIMEOUT_MS);
63
65
  try {
64
66
  const response = await fetch(target, { ...init, redirect: 'error', signal });
65
67
  if (!response.ok) {
@@ -134,6 +136,7 @@ async function modelProxyProbe(config, entry) {
134
136
  }
135
137
  if (entry.credential && Date.parse(entry.credential.expiresAt) <= Date.now() + CREDENTIAL_RENEW_BEFORE_MS)
136
138
  entry.credential = undefined;
139
+ const reusedCredential = Boolean(entry.credential);
137
140
  if (!entry.credential) {
138
141
  const response = await requestProbe(credentialEndpoint, {
139
142
  method: 'POST', headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': config.environment },
@@ -152,8 +155,13 @@ async function modelProxyProbe(config, entry) {
152
155
  const target = new URL(platformEndpoint(config.publicBaseUrl, modelConnectivityPath(credential.configId)));
153
156
  target.searchParams.set('nonce', nonce);
154
157
  const result = await requestProbe(target, { method: 'GET', headers: { authorization: `Bearer ${credential.token}`, 'playcraft-env': config.environment } });
155
- if (result.state.reason === 'endpoint_rejected')
158
+ if (result.state.reason === 'endpoint_rejected') {
156
159
  entry.credential = undefined;
160
+ // A restore can invalidate an otherwise unexpired credential. Reissue once for this
161
+ // read-only echo; a fresh refusal remains a refusal. Model turns are never replayed here.
162
+ if (reusedCredential)
163
+ return modelProxyProbe(config, entry);
164
+ }
157
165
  if (result.state.status !== 'ready')
158
166
  return result.state;
159
167
  return echoMatches(result.body, { service: 'playcraft-model-proxy', ...scope(config), configId: credential.configId, nonce })
@@ -1241,7 +1241,11 @@ async function releaseManagedWrite(state, writeId, workspaceId) {
1241
1241
  await state.journal.putManagedWrite({ ...lease, stoppedAt: new Date().toISOString() });
1242
1242
  return true;
1243
1243
  }
1244
+ const nativeCollectors = new WeakSet();
1244
1245
  export function attachNativeCollector(state) {
1246
+ if (nativeCollectors.has(state))
1247
+ return;
1248
+ nativeCollectors.add(state);
1245
1249
  state.native?.onCollected?.((event) => {
1246
1250
  void ingestCollected(state, event).then(async () => {
1247
1251
  if (event.kind !== 'notification' || !event.method)
@@ -25,6 +25,7 @@ import { loadWorkspaceSkillArtifact } from '../workspaces/skill-artifact-loader.
25
25
  import { join } from 'node:path';
26
26
  import { terminalHost } from './terminal-host.js';
27
27
  import { productionCapability, productionDirectoryBusy, executeSandboxProduction, sandboxProductionReceipt } from '../production/controller.js';
28
+ import { dispatchBatchRoute } from '../batch/routes.js';
28
29
  import { cleanupWorkspaceDirectories, syncWorkspaceDirectories } from '../workspaces/directory-maintenance.js';
29
30
  import { registerWorkspaceDirectory, restoreWorkspaceDirectory } from '../workspaces/directories.js';
30
31
  import { readWorkspaceDirectoryStats } from '../workspaces/directory-stats.js';
@@ -50,6 +51,11 @@ export async function handleRuntimeRequest(state, request, response) {
50
51
  return;
51
52
  }
52
53
  try {
54
+ const batch = await dispatchBatchRoute(state, request, url);
55
+ if (batch) {
56
+ writeJson(response, batch.status, batch.body);
57
+ return;
58
+ }
53
59
  if (request.method === 'GET' && url.pathname === '/runtime/production/capability') {
54
60
  writeJson(response, 200, await productionCapability(state));
55
61
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.43-beta",
3
+ "version": "0.1.45-beta",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -51,8 +51,8 @@
51
51
  "@gltf-transform/core": "^4.3.0",
52
52
  "@gltf-transform/extensions": "^4.3.0",
53
53
  "@gltf-transform/functions": "^4.3.0",
54
- "@playcraft/build": "^0.1.43-beta",
55
- "@playcraft/common": "^0.1.43-beta",
54
+ "@playcraft/build": "^0.1.45-beta",
55
+ "@playcraft/common": "^0.1.45-beta",
56
56
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
57
57
  "chokidar": "^4.0.3",
58
58
  "commander": "^13.1.0",
@@ -66,8 +66,8 @@
66
66
  },
67
67
  {
68
68
  "skillRef": "platform/playcraft-project-management",
69
- "version": "1.0.0",
70
- "artifactHash": "e7e4bffe80e0737362a5cac4234056a7d7fb048f0c8b5e41041e61ee914495e3",
69
+ "version": "1.1.0",
70
+ "artifactHash": "8653eefd40bc3b5a84728522f778515332c2334e64d90407ce2f8846a1f72e98",
71
71
  "surface": "portal-agent",
72
72
  "mountPath": ".playcraft/skills/platform__playcraft-project-management"
73
73
  },