@volter/world 3.0.178 → 3.0.180

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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/examples/customer-onboarding/README.md +3 -3
  3. package/dist/examples/customer-onboarding/package.json +13 -5
  4. package/dist/examples/customer-onboarding/pnpm-workspace.yaml +6 -0
  5. package/dist/examples/customer-onboarding/public-files.json +2 -2
  6. package/dist/examples/index.json +3 -0
  7. package/dist/examples/integration-branches/README.md +8 -8
  8. package/dist/examples/integration-branches/package.json +13 -5
  9. package/dist/examples/integration-branches/pnpm-workspace.yaml +6 -0
  10. package/dist/examples/integration-branches/public-files.json +2 -2
  11. package/dist/examples/workplace-agent/README.md +3 -3
  12. package/dist/examples/workplace-agent/package.json +13 -5
  13. package/dist/examples/workplace-agent/pnpm-workspace.yaml +6 -0
  14. package/dist/examples/workplace-agent/public-files.json +2 -2
  15. package/dist/skills/volter-world/AGENTS.md +7 -2
  16. package/dist/src/agent-prompt.js +2 -2
  17. package/dist/src/cli.js +4 -4
  18. package/dist/src/update-notice.js +1 -1
  19. package/dist/src/world.d.ts +1 -1
  20. package/dist/src/world.js +7 -5
  21. package/examples/customer-onboarding/README.md +3 -3
  22. package/examples/customer-onboarding/package.json +13 -5
  23. package/examples/customer-onboarding/pnpm-workspace.yaml +6 -0
  24. package/examples/customer-onboarding/public-files.json +2 -2
  25. package/examples/index.json +3 -0
  26. package/examples/integration-branches/README.md +8 -8
  27. package/examples/integration-branches/package.json +13 -5
  28. package/examples/integration-branches/pnpm-workspace.yaml +6 -0
  29. package/examples/integration-branches/public-files.json +2 -2
  30. package/examples/workplace-agent/README.md +3 -3
  31. package/examples/workplace-agent/package.json +13 -5
  32. package/examples/workplace-agent/pnpm-workspace.yaml +6 -0
  33. package/examples/workplace-agent/public-files.json +2 -2
  34. package/package.json +14 -6
  35. package/skills/volter-world/AGENTS.md +7 -2
  36. package/src/agent-prompt.ts +2 -2
  37. package/src/cli.ts +4 -4
  38. package/src/update-notice.ts +1 -1
  39. package/src/world.ts +7 -4
package/README.md CHANGED
@@ -4,7 +4,7 @@ Run your app against a world of twins. This package is the `volter` command and
4
4
  it: `World` and `TwinLog`, one method per verb.
5
5
 
6
6
  ```bash
7
- bun add -d @volter/world @volter/twin-stripe
7
+ pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world @volter/twin-stripe
8
8
  volter world init # reads the app, writes .volter/world.json beside the code
9
9
  volter world up # starts the twins, loads their default data
10
10
  volter world run -- npm test
@@ -13,12 +13,12 @@ You need Node 22.6+, npm and registry access during installation. The browser pa
13
13
  Copy this starter into a new folder:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.178 example customer-onboarding --dir onboarding
16
+ $ npx @volter/world@3.0.180 example customer-onboarding --dir onboarding
17
17
  $ cd onboarding
18
- $ npm install
18
+ $ pnpm install
19
19
  ```
20
20
 
21
- The command copies source; it neither installs dependencies nor starts a World. Its manifest pins the CLI, core, runtime, console, twins and SDKs. The installed starter creates a lockfile during installation. The repository's [package manifest](./package.json) and [lockfile](./package-lock.json) identify the source example's dependency cohort. Keep your own lockfile with the application when sharing it.
21
+ The command copies source; it neither installs dependencies nor starts a World. Its manifest pins the CLI, core, runtime, console, twins and SDKs. The copied starter retains its native pnpm settings. Its first install creates a lockfile; later installs use `pnpm install --frozen-lockfile`. The [package manifest](./package.json) identifies the selected dependency versions. Keep your own lockfile with the application when sharing it.
22
22
 
23
23
  Read [onboarding.mjs](./onboarding.mjs) for the application and [run.mjs](./run.mjs) for the command entry. Empty keys in [.env.example](./.env.example) tell init what the app reads; init supplies throwaway credentials. Do not add real keys.
24
24
 
@@ -11,12 +11,20 @@
11
11
  "pg": "8.16.3"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.178",
15
- "@volter/world-core": "3.0.160",
16
- "@volter/world-runtime": "3.0.175",
17
- "@volter/world-console": "3.0.153",
14
+ "@volter/world": "3.0.180",
15
+ "@volter/world-core": "3.0.161",
16
+ "@volter/world-runtime": "3.0.177",
17
+ "@volter/world-console": "3.0.152",
18
18
  "@volter/twin-stripe": "3.0.2",
19
19
  "@volter/twin-resend": "1.0.1",
20
- "@volter/world-ui": "1.0.160"
20
+ "@volter/world-ui": "1.0.161"
21
+ },
22
+ "packageManager": "pnpm@12.15.0",
23
+ "devEngines": {
24
+ "packageManager": {
25
+ "name": "pnpm",
26
+ "version": "12.15.0",
27
+ "onFail": "error"
28
+ }
21
29
  }
22
30
  }
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - .
3
+ packageImportMethod: hardlink
4
+ nodeLinker: hoisted
5
+ autoInstallPeers: true
6
+ strictPeerDependencies: false
@@ -4,11 +4,11 @@
4
4
  "README.md",
5
5
  "demo.mjs",
6
6
  "onboarding.mjs",
7
- "package-lock.json",
8
7
  "package.json",
9
8
  "prepare-ui.mjs",
10
9
  "public-files.json",
11
10
  "run.mjs",
12
11
  "server.mjs",
13
- "style.css"
12
+ "style.css",
13
+ "pnpm-workspace.yaml"
14
14
  ]
@@ -14,6 +14,7 @@
14
14
  "run.mjs",
15
15
  "server.mjs",
16
16
  "style.css",
17
+ "pnpm-workspace.yaml",
17
18
  "LICENSE"
18
19
  ]
19
20
  },
@@ -33,6 +34,7 @@
33
34
  "read-slack-mail.mjs",
34
35
  "reset-receipts.mjs",
35
36
  "seed.mjs",
37
+ "pnpm-workspace.yaml",
36
38
  "LICENSE"
37
39
  ]
38
40
  },
@@ -47,6 +49,7 @@
47
49
  "package.json",
48
50
  "public-files.json",
49
51
  "receipt.mjs",
52
+ "pnpm-workspace.yaml",
50
53
  "LICENSE"
51
54
  ]
52
55
  }
@@ -4,18 +4,18 @@ title: Shared baseline, workers and integration
4
4
 
5
5
  # Shared baseline, workers and integration
6
6
 
7
- Give two workers the same Stripe baseline, let them change different existing customers in isolated Worlds, then send their changesets to a separate integration World. This local example needs Node 22.6+, npm and registry access during preparation. It requires no platform account, shared host or real vendor key.
7
+ Give two workers the same Stripe baseline, let them change different existing customers in isolated Worlds, then send their changesets to a separate integration World. This local example needs Node 22.6+, pnpm 12.15.0 and registry access during preparation. It requires no platform account, shared host or real vendor key.
8
8
 
9
- [package.json](./package.json) and [package-lock.json](./package-lock.json) identify the source cohort. Each copied starter installs exact manifest pins and keeps its own lockfile. [customer.mjs](./customer.mjs) uses Stripe's real SDK. [receipt.mjs](./receipt.mjs) shows the current World, configured remotes, tracked origin, unpushed work and each changeset's actual destination.
9
+ [package.json](./package.json) identifies the selected dependency versions. Each copied starter installs exact manifest pins and keeps the lockfile created by its first installation for frozen reentry. [customer.mjs](./customer.mjs) uses Stripe's real SDK. [receipt.mjs](./receipt.mjs) shows the current World, configured remotes, tracked origin, unpushed work and each changeset's actual destination.
10
10
 
11
11
  ## Make the baseline
12
12
 
13
13
  Use an empty parent folder. Create the baseline starter there:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.178 example integration-branches --dir baseline
16
+ $ npx @volter/world@3.0.180 example integration-branches --dir baseline
17
17
  $ cd baseline
18
- $ npm install
18
+ $ pnpm install
19
19
  $ ./node_modules/.bin/volter world init --name baseline --twins stripe
20
20
  ```
21
21
 
@@ -38,16 +38,16 @@ The stopped baseline retains its history and can supply local path remotes. Defa
38
38
  Create three more copies in the same parent folder:
39
39
 
40
40
  ```console
41
- $ npx @volter/world@3.0.178 example integration-branches --dir integration
42
- $ npx @volter/world@3.0.178 example integration-branches --dir worker-a
43
- $ npx @volter/world@3.0.178 example integration-branches --dir worker-b
41
+ $ npx @volter/world@3.0.180 example integration-branches --dir integration
42
+ $ npx @volter/world@3.0.180 example integration-branches --dir worker-a
43
+ $ npx @volter/world@3.0.180 example integration-branches --dir worker-b
44
44
  ```
45
45
 
46
46
  In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
47
47
 
48
48
  ```console
49
49
  $ cd integration
50
- $ npm install
50
+ $ pnpm install --frozen-lockfile
51
51
  $ ./node_modules/.bin/volter world init --name integration --twins stripe
52
52
  $ ./node_modules/.bin/volter remote add origin ../baseline
53
53
  $ ./node_modules/.bin/volter world pull
@@ -9,11 +9,19 @@
9
9
  "stripe": "17.7.0"
10
10
  },
11
11
  "devDependencies": {
12
- "@volter/world": "3.0.178",
13
- "@volter/world-core": "3.0.160",
14
- "@volter/world-runtime": "3.0.175",
15
- "@volter/world-console": "3.0.153",
12
+ "@volter/world": "3.0.180",
13
+ "@volter/world-core": "3.0.161",
14
+ "@volter/world-runtime": "3.0.177",
15
+ "@volter/world-console": "3.0.152",
16
16
  "@volter/twin-stripe": "3.0.2",
17
- "@volter/world-ui": "1.0.160"
17
+ "@volter/world-ui": "1.0.161"
18
+ },
19
+ "packageManager": "pnpm@12.15.0",
20
+ "devEngines": {
21
+ "packageManager": {
22
+ "name": "pnpm",
23
+ "version": "12.15.0",
24
+ "onFail": "error"
25
+ }
18
26
  }
19
27
  }
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - .
3
+ packageImportMethod: hardlink
4
+ nodeLinker: hoisted
5
+ autoInstallPeers: true
6
+ strictPeerDependencies: false
@@ -3,8 +3,8 @@
3
3
  ".gitignore",
4
4
  "README.md",
5
5
  "customer.mjs",
6
- "package-lock.json",
7
6
  "package.json",
8
7
  "public-files.json",
9
- "receipt.mjs"
8
+ "receipt.mjs",
9
+ "pnpm-workspace.yaml"
10
10
  ]
@@ -19,13 +19,13 @@ A catalog page may select a newer default. Keep this example's manifest and inst
19
19
  together; its recorded workflow does not establish behavior for a different release.
20
20
 
21
21
  ```console
22
- $ npx @volter/world@3.0.178 example workplace-agent --dir workplace
22
+ $ npx @volter/world@3.0.180 example workplace-agent --dir workplace
23
23
  $ cd workplace
24
- $ npm install
24
+ $ pnpm install
25
25
  $ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
26
26
  ```
27
27
 
28
- Read [package.json](./package.json), [package-lock.json](./package-lock.json) and [clients.mjs](./clients.mjs) for exact tool, twin and SDK versions. Keep the lockfile created by your installation. Review the three configured vendors and synthetic credentials before booting. No model vendor is selected by this starter.
28
+ Read [package.json](./package.json) and [clients.mjs](./clients.mjs) for exact tool, twin and SDK versions. Keep the supplied native pnpm settings and the lockfile created by installation with the application; later installs use `pnpm install --frozen-lockfile`. Review the three configured vendors and synthetic credentials before booting. No model vendor is selected by this starter.
29
29
 
30
30
  ```console
31
31
  $ ./node_modules/.bin/volter world up
@@ -11,13 +11,21 @@
11
11
  "@slack/web-api": "7.19.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.178",
15
- "@volter/world-core": "3.0.160",
16
- "@volter/world-runtime": "3.0.175",
17
- "@volter/world-console": "3.0.153",
14
+ "@volter/world": "3.0.180",
15
+ "@volter/world-core": "3.0.161",
16
+ "@volter/world-runtime": "3.0.177",
17
+ "@volter/world-console": "3.0.152",
18
18
  "@volter/twin-github": "3.0.9",
19
19
  "@volter/twin-linear": "3.0.5",
20
20
  "@volter/twin-slack": "3.0.8",
21
- "@volter/world-ui": "1.0.160"
21
+ "@volter/world-ui": "1.0.161"
22
+ },
23
+ "packageManager": "pnpm@12.15.0",
24
+ "devEngines": {
25
+ "packageManager": {
26
+ "name": "pnpm",
27
+ "version": "12.15.0",
28
+ "onFail": "error"
29
+ }
22
30
  }
23
31
  }
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - .
3
+ packageImportMethod: hardlink
4
+ nodeLinker: hoisted
5
+ autoInstallPeers: true
6
+ strictPeerDependencies: false
@@ -6,10 +6,10 @@
6
6
  "create-organization.mjs",
7
7
  "handoff.mjs",
8
8
  "operate.mjs",
9
- "package-lock.json",
10
9
  "package.json",
11
10
  "public-files.json",
12
11
  "read-slack-mail.mjs",
13
12
  "reset-receipts.mjs",
14
- "seed.mjs"
13
+ "seed.mjs",
14
+ "pnpm-workspace.yaml"
15
15
  ]
@@ -26,8 +26,13 @@ volter world log # what the app did at each vendor
26
26
  ```
27
27
 
28
28
  Show the person the vendors `init` detected, and why, before keeping them. Keys the app reads
29
- (named in `.env.example`) get throwaway values in the World's env; never put real credentials in
30
- a World to get a test past validation. `volter world view --no-open` runs the World with its
29
+ (named in `.env.example`) for selected twins get throwaway values in the World's env; never
30
+ replace a twin's credential with a real one to get a test past validation. An intentionally
31
+ omitted vendor uses its ordinary production SDK/API and the caller's own credential at launch,
32
+ subject to sandbox and network policy. Remove that vendor's fake values and endpoint overrides;
33
+ keep live credentials out of the config and generated World env. Follow
34
+ [turn a service off](../../docs/guides/turn-a-service-off.md), including its credential-launch order.
35
+ `volter world view --no-open` runs the World with its
31
36
  dashboard and prints its link for the person; it keeps running, so start it in the background,
32
37
  and stop a World you started with `up` first (`down` keeps its state).
33
38
 
@@ -8,8 +8,8 @@ export function agentPrompt(o = {}) {
8
8
  'Set up Volter World for the workflow I want to run in this app. Select only the vendors that workflow deliberately substitutes with local twins. Keep the app\'s real SDKs.',
9
9
  '',
10
10
  o.tools
11
- ? '1. You have the Volter World tools (volter mcp). Keep an app\'s existing pinned World tools. If the `volter` command is missing, install it: `npm install -g @volter/world`.'
12
- : '1. Keep the app\'s pinned World tools if present, running `./node_modules/.bin/volter` from the app folder after installing its locked dependencies. A missing local binary stops without fetching another package. Otherwise install the command: `npm install -g @volter/world`. Install the agent tooling with `volter agents install --yes`.',
11
+ ? '1. You have the Volter World tools (volter mcp). Keep an app\'s existing pinned World tools. If the `volter` command is missing, install it: `pnpm add -g --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world`.'
12
+ : '1. Keep the app\'s pinned World tools if present, running `./node_modules/.bin/volter` from the app folder after installing its locked dependencies. A missing local binary stops without fetching another package. Otherwise install the command: `pnpm add -g --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world`. Install the agent tooling with `volter agents install --yes`.',
13
13
  `2. Read the app's manifests, env names and any .volter/world.json. Show me the likely vendors and why, before keeping them. Preserve a saved World and its state; do not initialize it again to resume. For a new World, choose the exact catalog releases and initialize only the selected vendors with ${cmd('volter world init --twins <selected-vendors> --source <vendor>=<package>', 'world_init with vendors and sources')}.`,
14
14
  `3. ${o.tools ? 'Start it with world_up' : 'Start it: `volter world up`'}, then run my usual app command inside it: ${cmd('volter world run -- <the app command>', 'world_run')}. Use throwaway credentials. Diagnose a failure as routing, data, scenario, unsupported operation or runtime; preserve the refusal's evidence rather than bypassing it.`,
15
15
  `4. Tell me what the app did at each vendor (${cmd('volter world log', 'world_log')}), and give me the dashboard link (${o.tools ? 'world_view' : '`volter world view --no-open`, started in the background: it keeps running'}).`,
package/dist/src/cli.js CHANGED
@@ -515,7 +515,7 @@ async function worldCommand(verb, args) {
515
515
  // the read token's own one-line way in, for handing to someone who should look and not touch
516
516
  // (not on a local World: any of its pages can open a write session there, so a read-only link restricts nothing)
517
517
  const readOnly = view.console && !(local && view.origin) ? `${view.console}#token=${encodeURIComponent(view.readToken)}` : null;
518
- out(`${local && view.origin ? `apps the World's tokens are in .volter/token and .volter/token.read (apps and scripts present them; the browser needs none)` : verb === 'serve' ? '' : `token ${view.token}\nread ${view.readToken}`}${readOnly ? `\nread-only view ${readOnly}` : ''}${view.console ? '' : '\n(no console installed: `npm install -D @volter/world-console` to look around in a browser)'}${view.origin ? '\n(the World opens at its own *.localhost origin; if your browser cannot reach *.localhost, run again with --no-origins)' : ''}`);
518
+ out(`${local && view.origin ? `apps the World's tokens are in .volter/token and .volter/token.read (apps and scripts present them; the browser needs none)` : verb === 'serve' ? '' : `token ${view.token}\nread ${view.readToken}`}${readOnly ? `\nread-only view ${readOnly}` : ''}${view.console ? '' : '\n(no console installed: `pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world-console` to look around in a browser)'}${view.origin ? '\n(the World opens at its own *.localhost origin; if your browser cannot reach *.localhost, run again with --no-origins)' : ''}`);
519
519
  if (open && verb === 'view' && !args.includes('--no-open'))
520
520
  openInBrowser(open);
521
521
  await waitForServeShutdown(() => view.stop());
@@ -699,7 +699,7 @@ async function twinCommand(args) {
699
699
  }
700
700
  const dir = resolveCatalog(process.cwd()).packDir(vendor);
701
701
  if (dir === undefined)
702
- throw new Error(`No twin "${vendor}" here — \`bun add -d @volter/twin-${vendor}\`, or run from a checkout that has it`);
702
+ throw new Error(`No twin "${vendor}" here — \`pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/twin-${vendor}\`, or run from a checkout that has it`);
703
703
  if (rest.length === 0 || rest[0].startsWith('-'))
704
704
  throw new Error(`volter twin ${vendor}: which verb? serve | root | credential | refresh`);
705
705
  const cli = packCli(dir);
@@ -718,7 +718,7 @@ function remoteCli() {
718
718
  const here = resolve(import.meta.dir, '..', '..', 'world-host', 'src', 'cli.ts');
719
719
  if (existsSync(here))
720
720
  return here;
721
- throw new Error('volter remote: the hosting product (@volter/world-host) is not installed — `bun add -d @volter/world-host`');
721
+ throw new Error('volter remote: the hosting product (@volter/world-host) is not installed — `pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world-host`');
722
722
  }
723
723
  /** `volter login <platform url>`: the CLI signed into the hosted platform as you, through the browser (the device
724
724
  * authorization grant, as `gh auth login`): the platform shows a code, you approve it there signed in as yourself,
@@ -1067,7 +1067,7 @@ async function installDetectedTwins(app, install, selection = {}) {
1067
1067
  });
1068
1068
  const has = (file) => existsSync(join(app, file));
1069
1069
  // the app's own package manager, by its lockfile; npm where there is none
1070
- const invocation = has('bun.lock') || has('bun.lockb') ? ['bun', 'add', '-d'] : has('pnpm-lock.yaml') ? ['pnpm', 'add', '-D'] : has('yarn.lock') ? ['yarn', 'add', '-D'] : ['npm', 'install', '-D'];
1070
+ const invocation = has('bun.lock') || has('bun.lockb') ? ['bun', 'add', '-d'] : has('pnpm-lock.yaml') ? ['pnpm', 'add', '-D', '--package-import-method=hardlink', '--config.nodeLinker=hoisted'] : has('yarn.lock') ? ['yarn', 'add', '-D'] : ['npm', 'install', '-D'];
1071
1071
  const command = [...invocation, ...pkgs].join(' ');
1072
1072
  out(`detected ${vendors.map((v) => `${v} (${(found.get(v) ?? []).slice(0, 2).join(', ')})`).join('; ')}`);
1073
1073
  out(`twins ${pkgs.join(' ')}`);
@@ -61,6 +61,6 @@ export function startUpdateCheck(args, env = process.env) {
61
61
  return async () => {
62
62
  const latest = await Promise.race([check, new Promise((r) => setTimeout(() => r(null), 800))]);
63
63
  if (latest && newer(latest, here))
64
- process.stderr.write(`\nvolter ${latest} is out (you have ${here}): npm install -g @volter/world\n`);
64
+ process.stderr.write(`\nvolter ${latest} is out (you have ${here}): pnpm add -g --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world\n`);
65
65
  };
66
66
  }
@@ -79,7 +79,7 @@ export declare class World {
79
79
  /** Whether `from` (default: the cwd) is inside a world. */
80
80
  static find(from?: string): World | null;
81
81
  /** `volter world init --bare <org>/<world> --twins a,b`: a world with no app — a package.json naming the
82
- * twins (installed with bun), then the world over them, served as `/<org>/<world>/`. */
82
+ * twins (installed with pnpm), then the world over them, served as `/<org>/<world>/`. */
83
83
  static initBare(dir: string, served: string, twins: string[], opts?: {
84
84
  install?: boolean;
85
85
  force?: boolean;
package/dist/src/world.js CHANGED
@@ -82,7 +82,7 @@ export class World {
82
82
  return root === null ? null : new World(currentBranch(root), root);
83
83
  }
84
84
  /** `volter world init --bare <org>/<world> --twins a,b`: a world with no app — a package.json naming the
85
- * twins (installed with bun), then the world over them, served as `/<org>/<world>/`. */
85
+ * twins (installed with pnpm), then the world over them, served as `/<org>/<world>/`. */
86
86
  static initBare(dir, served, twins, opts = {}) {
87
87
  if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/i.test(served))
88
88
  throw new Error(`a bare world is named <org>/<world>, got ${served}`);
@@ -90,11 +90,13 @@ export class World {
90
90
  throw new Error('a bare world needs its twins: --twins github,slack');
91
91
  mkdirSync(dir, { recursive: true });
92
92
  const pkgPath = join(dir, 'package.json');
93
- if (!existsSync(pkgPath))
94
- writeFileSync(pkgPath, `${JSON.stringify({ name: served.split('/')[1], private: true, devDependencies: Object.fromEntries(twins.map((t) => [`@volter/twin-${t}`, '*'])) }, null, 2)}\n`);
93
+ if (!existsSync(pkgPath)) {
94
+ writeFileSync(pkgPath, `${JSON.stringify({ name: served.split('/')[1], private: true, packageManager: 'pnpm@12.15.0', devEngines: { packageManager: { name: 'pnpm', version: '12.15.0', onFail: 'error' } }, devDependencies: Object.fromEntries(twins.map((t) => [`@volter/twin-${t}`, '*'])) }, null, 2)}\n`);
95
+ writeFileSync(join(dir, 'pnpm-workspace.yaml'), 'packages:\n - .\npackageImportMethod: hardlink\nnodeLinker: hoisted\nautoInstallPeers: true\nstrictPeerDependencies: false\n');
96
+ }
95
97
  if (opts.install !== false && !twins.every((t) => findInstalledPackage(dir, `@volter/twin-${t}`))) {
96
- // the runtime's own installer: bun under Bun, npm under Node — never assumed
97
- const cmd = typeof Bun !== 'undefined' ? ['bun', 'add', '-d'] : ['npm', 'install', '--save-dev'];
98
+ // Owned bare Worlds use the native store on their volume, independent of the runtime.
99
+ const cmd = ['pnpm', 'add', '--save-dev', '--package-import-method=hardlink', '--config.nodeLinker=hoisted'];
98
100
  const r = spawnSync(cmd[0], [...cmd.slice(1), ...twins.map((t) => `@volter/twin-${t}`)], { windowsHide: true, cwd: dir, stdio: 'inherit', env: withoutVaultSignIn(process.env) });
99
101
  if (r.status !== 0)
100
102
  throw new Error(`installing the twins failed (${cmd.join(' ')} exited ${r.status})`);
@@ -13,12 +13,12 @@ You need Node 22.6+, npm and registry access during installation. The browser pa
13
13
  Copy this starter into a new folder:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.178 example customer-onboarding --dir onboarding
16
+ $ npx @volter/world@3.0.180 example customer-onboarding --dir onboarding
17
17
  $ cd onboarding
18
- $ npm install
18
+ $ pnpm install
19
19
  ```
20
20
 
21
- The command copies source; it neither installs dependencies nor starts a World. Its manifest pins the CLI, core, runtime, console, twins and SDKs. The installed starter creates a lockfile during installation. The repository's [package manifest](./package.json) and [lockfile](./package-lock.json) identify the source example's dependency cohort. Keep your own lockfile with the application when sharing it.
21
+ The command copies source; it neither installs dependencies nor starts a World. Its manifest pins the CLI, core, runtime, console, twins and SDKs. The copied starter retains its native pnpm settings. Its first install creates a lockfile; later installs use `pnpm install --frozen-lockfile`. The [package manifest](./package.json) identifies the selected dependency versions. Keep your own lockfile with the application when sharing it.
22
22
 
23
23
  Read [onboarding.mjs](./onboarding.mjs) for the application and [run.mjs](./run.mjs) for the command entry. Empty keys in [.env.example](./.env.example) tell init what the app reads; init supplies throwaway credentials. Do not add real keys.
24
24
 
@@ -11,12 +11,20 @@
11
11
  "pg": "8.16.3"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.178",
15
- "@volter/world-core": "3.0.160",
16
- "@volter/world-runtime": "3.0.175",
17
- "@volter/world-console": "3.0.153",
14
+ "@volter/world": "3.0.180",
15
+ "@volter/world-core": "3.0.161",
16
+ "@volter/world-runtime": "3.0.177",
17
+ "@volter/world-console": "3.0.152",
18
18
  "@volter/twin-stripe": "3.0.2",
19
19
  "@volter/twin-resend": "1.0.1",
20
- "@volter/world-ui": "1.0.160"
20
+ "@volter/world-ui": "1.0.161"
21
+ },
22
+ "packageManager": "pnpm@12.15.0",
23
+ "devEngines": {
24
+ "packageManager": {
25
+ "name": "pnpm",
26
+ "version": "12.15.0",
27
+ "onFail": "error"
28
+ }
21
29
  }
22
30
  }
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - .
3
+ packageImportMethod: hardlink
4
+ nodeLinker: hoisted
5
+ autoInstallPeers: true
6
+ strictPeerDependencies: false
@@ -4,11 +4,11 @@
4
4
  "README.md",
5
5
  "demo.mjs",
6
6
  "onboarding.mjs",
7
- "package-lock.json",
8
7
  "package.json",
9
8
  "prepare-ui.mjs",
10
9
  "public-files.json",
11
10
  "run.mjs",
12
11
  "server.mjs",
13
- "style.css"
12
+ "style.css",
13
+ "pnpm-workspace.yaml"
14
14
  ]
@@ -14,6 +14,7 @@
14
14
  "run.mjs",
15
15
  "server.mjs",
16
16
  "style.css",
17
+ "pnpm-workspace.yaml",
17
18
  "LICENSE"
18
19
  ]
19
20
  },
@@ -33,6 +34,7 @@
33
34
  "read-slack-mail.mjs",
34
35
  "reset-receipts.mjs",
35
36
  "seed.mjs",
37
+ "pnpm-workspace.yaml",
36
38
  "LICENSE"
37
39
  ]
38
40
  },
@@ -47,6 +49,7 @@
47
49
  "package.json",
48
50
  "public-files.json",
49
51
  "receipt.mjs",
52
+ "pnpm-workspace.yaml",
50
53
  "LICENSE"
51
54
  ]
52
55
  }
@@ -4,18 +4,18 @@ title: Shared baseline, workers and integration
4
4
 
5
5
  # Shared baseline, workers and integration
6
6
 
7
- Give two workers the same Stripe baseline, let them change different existing customers in isolated Worlds, then send their changesets to a separate integration World. This local example needs Node 22.6+, npm and registry access during preparation. It requires no platform account, shared host or real vendor key.
7
+ Give two workers the same Stripe baseline, let them change different existing customers in isolated Worlds, then send their changesets to a separate integration World. This local example needs Node 22.6+, pnpm 12.15.0 and registry access during preparation. It requires no platform account, shared host or real vendor key.
8
8
 
9
- [package.json](./package.json) and [package-lock.json](./package-lock.json) identify the source cohort. Each copied starter installs exact manifest pins and keeps its own lockfile. [customer.mjs](./customer.mjs) uses Stripe's real SDK. [receipt.mjs](./receipt.mjs) shows the current World, configured remotes, tracked origin, unpushed work and each changeset's actual destination.
9
+ [package.json](./package.json) identifies the selected dependency versions. Each copied starter installs exact manifest pins and keeps the lockfile created by its first installation for frozen reentry. [customer.mjs](./customer.mjs) uses Stripe's real SDK. [receipt.mjs](./receipt.mjs) shows the current World, configured remotes, tracked origin, unpushed work and each changeset's actual destination.
10
10
 
11
11
  ## Make the baseline
12
12
 
13
13
  Use an empty parent folder. Create the baseline starter there:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.178 example integration-branches --dir baseline
16
+ $ npx @volter/world@3.0.180 example integration-branches --dir baseline
17
17
  $ cd baseline
18
- $ npm install
18
+ $ pnpm install
19
19
  $ ./node_modules/.bin/volter world init --name baseline --twins stripe
20
20
  ```
21
21
 
@@ -38,16 +38,16 @@ The stopped baseline retains its history and can supply local path remotes. Defa
38
38
  Create three more copies in the same parent folder:
39
39
 
40
40
  ```console
41
- $ npx @volter/world@3.0.178 example integration-branches --dir integration
42
- $ npx @volter/world@3.0.178 example integration-branches --dir worker-a
43
- $ npx @volter/world@3.0.178 example integration-branches --dir worker-b
41
+ $ npx @volter/world@3.0.180 example integration-branches --dir integration
42
+ $ npx @volter/world@3.0.180 example integration-branches --dir worker-a
43
+ $ npx @volter/world@3.0.180 example integration-branches --dir worker-b
44
44
  ```
45
45
 
46
46
  In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
47
47
 
48
48
  ```console
49
49
  $ cd integration
50
- $ npm install
50
+ $ pnpm install --frozen-lockfile
51
51
  $ ./node_modules/.bin/volter world init --name integration --twins stripe
52
52
  $ ./node_modules/.bin/volter remote add origin ../baseline
53
53
  $ ./node_modules/.bin/volter world pull
@@ -9,11 +9,19 @@
9
9
  "stripe": "17.7.0"
10
10
  },
11
11
  "devDependencies": {
12
- "@volter/world": "3.0.178",
13
- "@volter/world-core": "3.0.160",
14
- "@volter/world-runtime": "3.0.175",
15
- "@volter/world-console": "3.0.153",
12
+ "@volter/world": "3.0.180",
13
+ "@volter/world-core": "3.0.161",
14
+ "@volter/world-runtime": "3.0.177",
15
+ "@volter/world-console": "3.0.152",
16
16
  "@volter/twin-stripe": "3.0.2",
17
- "@volter/world-ui": "1.0.160"
17
+ "@volter/world-ui": "1.0.161"
18
+ },
19
+ "packageManager": "pnpm@12.15.0",
20
+ "devEngines": {
21
+ "packageManager": {
22
+ "name": "pnpm",
23
+ "version": "12.15.0",
24
+ "onFail": "error"
25
+ }
18
26
  }
19
27
  }
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - .
3
+ packageImportMethod: hardlink
4
+ nodeLinker: hoisted
5
+ autoInstallPeers: true
6
+ strictPeerDependencies: false
@@ -3,8 +3,8 @@
3
3
  ".gitignore",
4
4
  "README.md",
5
5
  "customer.mjs",
6
- "package-lock.json",
7
6
  "package.json",
8
7
  "public-files.json",
9
- "receipt.mjs"
8
+ "receipt.mjs",
9
+ "pnpm-workspace.yaml"
10
10
  ]
@@ -19,13 +19,13 @@ A catalog page may select a newer default. Keep this example's manifest and inst
19
19
  together; its recorded workflow does not establish behavior for a different release.
20
20
 
21
21
  ```console
22
- $ npx @volter/world@3.0.178 example workplace-agent --dir workplace
22
+ $ npx @volter/world@3.0.180 example workplace-agent --dir workplace
23
23
  $ cd workplace
24
- $ npm install
24
+ $ pnpm install
25
25
  $ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
26
26
  ```
27
27
 
28
- Read [package.json](./package.json), [package-lock.json](./package-lock.json) and [clients.mjs](./clients.mjs) for exact tool, twin and SDK versions. Keep the lockfile created by your installation. Review the three configured vendors and synthetic credentials before booting. No model vendor is selected by this starter.
28
+ Read [package.json](./package.json) and [clients.mjs](./clients.mjs) for exact tool, twin and SDK versions. Keep the supplied native pnpm settings and the lockfile created by installation with the application; later installs use `pnpm install --frozen-lockfile`. Review the three configured vendors and synthetic credentials before booting. No model vendor is selected by this starter.
29
29
 
30
30
  ```console
31
31
  $ ./node_modules/.bin/volter world up
@@ -11,13 +11,21 @@
11
11
  "@slack/web-api": "7.19.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.178",
15
- "@volter/world-core": "3.0.160",
16
- "@volter/world-runtime": "3.0.175",
17
- "@volter/world-console": "3.0.153",
14
+ "@volter/world": "3.0.180",
15
+ "@volter/world-core": "3.0.161",
16
+ "@volter/world-runtime": "3.0.177",
17
+ "@volter/world-console": "3.0.152",
18
18
  "@volter/twin-github": "3.0.9",
19
19
  "@volter/twin-linear": "3.0.5",
20
20
  "@volter/twin-slack": "3.0.8",
21
- "@volter/world-ui": "1.0.160"
21
+ "@volter/world-ui": "1.0.161"
22
+ },
23
+ "packageManager": "pnpm@12.15.0",
24
+ "devEngines": {
25
+ "packageManager": {
26
+ "name": "pnpm",
27
+ "version": "12.15.0",
28
+ "onFail": "error"
29
+ }
22
30
  }
23
31
  }
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - .
3
+ packageImportMethod: hardlink
4
+ nodeLinker: hoisted
5
+ autoInstallPeers: true
6
+ strictPeerDependencies: false
@@ -6,10 +6,10 @@
6
6
  "create-organization.mjs",
7
7
  "handoff.mjs",
8
8
  "operate.mjs",
9
- "package-lock.json",
10
9
  "package.json",
11
10
  "public-files.json",
12
11
  "read-slack-mail.mjs",
13
12
  "reset-receipts.mjs",
14
- "seed.mjs"
13
+ "seed.mjs",
14
+ "pnpm-workspace.yaml"
15
15
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world",
3
- "version": "3.0.178",
3
+ "version": "3.0.180",
4
4
  "description": "Run your app against a world of twins. `World` and `Repo` — one method per verb: init, up, run, log, diff, reset, branch, checkout, clone, fetch, changeset, push — and the `volter` command, one client of them.",
5
5
  "keywords": [
6
6
  "volter",
@@ -31,12 +31,12 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.30.1",
34
- "@volter/world-core": "3.0.160",
35
- "@volter/world-runtime": "3.0.175",
34
+ "@volter/world-core": "3.0.161",
35
+ "@volter/world-runtime": "3.0.177",
36
36
  "zod": "^4.6.5"
37
37
  },
38
38
  "peerDependencies": {
39
- "@volter/world-console": "3.0.153"
39
+ "@volter/world-console": "3.0.152"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@volter/world-console": {
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@volter/world-console": "3.0.153"
47
+ "@volter/world-console": "3.0.152"
48
48
  },
49
49
  "bin": {
50
50
  "volter": "dist/src/cli.js"
@@ -54,5 +54,13 @@
54
54
  "prepack": "node ../../scripts/prepare-world-examples.mjs && node ./scripts/ship-skill.mjs in && node ../../scripts/publish/prepare-publish.mjs prepack",
55
55
  "postpack": "node ../../scripts/publish/prepare-publish.mjs postpack && node ./scripts/ship-skill.mjs out"
56
56
  },
57
- "homepage": "https://world.volter.ai/docs/getting-started"
57
+ "homepage": "https://world.volter.ai/docs/getting-started",
58
+ "packageManager": "pnpm@12.15.0",
59
+ "devEngines": {
60
+ "packageManager": {
61
+ "name": "pnpm",
62
+ "version": "12.15.0",
63
+ "onFail": "error"
64
+ }
65
+ }
58
66
  }
@@ -26,8 +26,13 @@ volter world log # what the app did at each vendor
26
26
  ```
27
27
 
28
28
  Show the person the vendors `init` detected, and why, before keeping them. Keys the app reads
29
- (named in `.env.example`) get throwaway values in the World's env; never put real credentials in
30
- a World to get a test past validation. `volter world view --no-open` runs the World with its
29
+ (named in `.env.example`) for selected twins get throwaway values in the World's env; never
30
+ replace a twin's credential with a real one to get a test past validation. An intentionally
31
+ omitted vendor uses its ordinary production SDK/API and the caller's own credential at launch,
32
+ subject to sandbox and network policy. Remove that vendor's fake values and endpoint overrides;
33
+ keep live credentials out of the config and generated World env. Follow
34
+ [turn a service off](../../docs/guides/turn-a-service-off.md), including its credential-launch order.
35
+ `volter world view --no-open` runs the World with its
31
36
  dashboard and prints its link for the person; it keeps running, so start it in the background,
32
37
  and stop a World you started with `up` first (`down` keeps its state).
33
38
 
@@ -18,8 +18,8 @@ export function agentPrompt(o: AgentPromptOptions = {}): string {
18
18
  'Set up Volter World for the workflow I want to run in this app. Select only the vendors that workflow deliberately substitutes with local twins. Keep the app\'s real SDKs.',
19
19
  '',
20
20
  o.tools
21
- ? '1. You have the Volter World tools (volter mcp). Keep an app\'s existing pinned World tools. If the `volter` command is missing, install it: `npm install -g @volter/world`.'
22
- : '1. Keep the app\'s pinned World tools if present, running `./node_modules/.bin/volter` from the app folder after installing its locked dependencies. A missing local binary stops without fetching another package. Otherwise install the command: `npm install -g @volter/world`. Install the agent tooling with `volter agents install --yes`.',
21
+ ? '1. You have the Volter World tools (volter mcp). Keep an app\'s existing pinned World tools. If the `volter` command is missing, install it: `pnpm add -g --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world`.'
22
+ : '1. Keep the app\'s pinned World tools if present, running `./node_modules/.bin/volter` from the app folder after installing its locked dependencies. A missing local binary stops without fetching another package. Otherwise install the command: `pnpm add -g --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world`. Install the agent tooling with `volter agents install --yes`.',
23
23
  `2. Read the app's manifests, env names and any .volter/world.json. Show me the likely vendors and why, before keeping them. Preserve a saved World and its state; do not initialize it again to resume. For a new World, choose the exact catalog releases and initialize only the selected vendors with ${cmd('volter world init --twins <selected-vendors> --source <vendor>=<package>', 'world_init with vendors and sources')}.`,
24
24
  `3. ${o.tools ? 'Start it with world_up' : 'Start it: `volter world up`'}, then run my usual app command inside it: ${cmd('volter world run -- <the app command>', 'world_run')}. Use throwaway credentials. Diagnose a failure as routing, data, scenario, unsupported operation or runtime; preserve the refusal's evidence rather than bypassing it.`,
25
25
  `4. Tell me what the app did at each vendor (${cmd('volter world log', 'world_log')}), and give me the dashboard link (${o.tools ? 'world_view' : '`volter world view --no-open`, started in the background: it keeps running'}).`,
package/src/cli.ts CHANGED
@@ -389,7 +389,7 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
389
389
  // the read token's own one-line way in, for handing to someone who should look and not touch
390
390
  // (not on a local World: any of its pages can open a write session there, so a read-only link restricts nothing)
391
391
  const readOnly = view.console && !(local && view.origin) ? `${view.console}#token=${encodeURIComponent(view.readToken)}` : null;
392
- out(`${local && view.origin ? `apps the World's tokens are in .volter/token and .volter/token.read (apps and scripts present them; the browser needs none)` : verb === 'serve' ? '' : `token ${view.token}\nread ${view.readToken}`}${readOnly ? `\nread-only view ${readOnly}` : ''}${view.console ? '' : '\n(no console installed: `npm install -D @volter/world-console` to look around in a browser)'}${view.origin ? '\n(the World opens at its own *.localhost origin; if your browser cannot reach *.localhost, run again with --no-origins)' : ''}`);
392
+ out(`${local && view.origin ? `apps the World's tokens are in .volter/token and .volter/token.read (apps and scripts present them; the browser needs none)` : verb === 'serve' ? '' : `token ${view.token}\nread ${view.readToken}`}${readOnly ? `\nread-only view ${readOnly}` : ''}${view.console ? '' : '\n(no console installed: `pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world-console` to look around in a browser)'}${view.origin ? '\n(the World opens at its own *.localhost origin; if your browser cannot reach *.localhost, run again with --no-origins)' : ''}`);
393
393
  if (open && verb === 'view' && !args.includes('--no-open')) openInBrowser(open);
394
394
  await waitForServeShutdown(() => view.stop());
395
395
  process.exit(0);
@@ -512,7 +512,7 @@ async function twinCommand(args: string[]): Promise<void> {
512
512
  return;
513
513
  }
514
514
  const dir = resolveCatalog(process.cwd()).packDir(vendor);
515
- if (dir === undefined) throw new Error(`No twin "${vendor}" here — \`bun add -d @volter/twin-${vendor}\`, or run from a checkout that has it`);
515
+ if (dir === undefined) throw new Error(`No twin "${vendor}" here — \`pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/twin-${vendor}\`, or run from a checkout that has it`);
516
516
  if (rest.length === 0 || rest[0]!.startsWith('-')) throw new Error(`volter twin ${vendor}: which verb? serve | root | credential | refresh`);
517
517
  const cli = packCli(dir); if (cli === undefined) throw new Error(`No cli for twin "${vendor}" (its manifest names no bin)`);
518
518
  process.exitCode = await passthrough([process.execPath, cli, ...rest]);
@@ -524,7 +524,7 @@ function remoteCli(): string {
524
524
  if (installed !== undefined) { const cli = packCli(installed); if (cli !== undefined) return cli; }
525
525
  const here = resolve(import.meta.dir, '..', '..', 'world-host', 'src', 'cli.ts');
526
526
  if (existsSync(here)) return here;
527
- throw new Error('volter remote: the hosting product (@volter/world-host) is not installed — `bun add -d @volter/world-host`');
527
+ throw new Error('volter remote: the hosting product (@volter/world-host) is not installed — `pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world-host`');
528
528
  }
529
529
 
530
530
  /** `volter login <platform url>`: the CLI signed into the hosted platform as you, through the browser (the device
@@ -791,7 +791,7 @@ async function installDetectedTwins(app: string, install: boolean, selection: {
791
791
  });
792
792
  const has = (file: string): boolean => existsSync(join(app, file));
793
793
  // the app's own package manager, by its lockfile; npm where there is none
794
- const invocation = has('bun.lock') || has('bun.lockb') ? ['bun', 'add', '-d'] : has('pnpm-lock.yaml') ? ['pnpm', 'add', '-D'] : has('yarn.lock') ? ['yarn', 'add', '-D'] : ['npm', 'install', '-D'];
794
+ const invocation = has('bun.lock') || has('bun.lockb') ? ['bun', 'add', '-d'] : has('pnpm-lock.yaml') ? ['pnpm', 'add', '-D', '--package-import-method=hardlink', '--config.nodeLinker=hoisted'] : has('yarn.lock') ? ['yarn', 'add', '-D'] : ['npm', 'install', '-D'];
795
795
  const command = [...invocation, ...pkgs].join(' ');
796
796
  out(`detected ${vendors.map((v) => `${v} (${(found.get(v) ?? []).slice(0, 2).join(', ')})`).join('; ')}`);
797
797
  out(`twins ${pkgs.join(' ')}`);
@@ -47,6 +47,6 @@ export function startUpdateCheck(args: string[], env: NodeJS.ProcessEnv = proces
47
47
  : Promise.resolve(kept?.latest ?? null);
48
48
  return async () => {
49
49
  const latest = await Promise.race([check, new Promise<null>((r) => setTimeout(() => r(null), 800))]);
50
- if (latest && newer(latest, here)) process.stderr.write(`\nvolter ${latest} is out (you have ${here}): npm install -g @volter/world\n`);
50
+ if (latest && newer(latest, here)) process.stderr.write(`\nvolter ${latest} is out (you have ${here}): pnpm add -g --package-import-method=hardlink --config.nodeLinker=hoisted @volter/world\n`);
51
51
  };
52
52
  }
package/src/world.ts CHANGED
@@ -99,16 +99,19 @@ export class World {
99
99
  return root === null ? null : new World(currentBranch(root), root);
100
100
  }
101
101
  /** `volter world init --bare <org>/<world> --twins a,b`: a world with no app — a package.json naming the
102
- * twins (installed with bun), then the world over them, served as `/<org>/<world>/`. */
102
+ * twins (installed with pnpm), then the world over them, served as `/<org>/<world>/`. */
103
103
  static initBare(dir: string, served: string, twins: string[], opts: { install?: boolean; force?: boolean } = {}): { world: World; result: InitResult } {
104
104
  if (!/^[a-z0-9_-]+\/[a-z0-9_-]+$/i.test(served)) throw new Error(`a bare world is named <org>/<world>, got ${served}`);
105
105
  if (twins.length === 0) throw new Error('a bare world needs its twins: --twins github,slack');
106
106
  mkdirSync(dir, { recursive: true });
107
107
  const pkgPath = join(dir, 'package.json');
108
- if (!existsSync(pkgPath)) writeFileSync(pkgPath, `${JSON.stringify({ name: served.split('/')[1], private: true, devDependencies: Object.fromEntries(twins.map((t) => [`@volter/twin-${t}`, '*'])) }, null, 2)}\n`);
108
+ if (!existsSync(pkgPath)) {
109
+ writeFileSync(pkgPath, `${JSON.stringify({ name: served.split('/')[1], private: true, packageManager: 'pnpm@12.15.0', devEngines: { packageManager: { name: 'pnpm', version: '12.15.0', onFail: 'error' } }, devDependencies: Object.fromEntries(twins.map((t) => [`@volter/twin-${t}`, '*'])) }, null, 2)}\n`);
110
+ writeFileSync(join(dir, 'pnpm-workspace.yaml'), 'packages:\n - .\npackageImportMethod: hardlink\nnodeLinker: hoisted\nautoInstallPeers: true\nstrictPeerDependencies: false\n');
111
+ }
109
112
  if (opts.install !== false && !twins.every((t) => findInstalledPackage(dir, `@volter/twin-${t}`))) {
110
- // the runtime's own installer: bun under Bun, npm under Node — never assumed
111
- const cmd = typeof Bun !== 'undefined' ? ['bun', 'add', '-d'] : ['npm', 'install', '--save-dev'];
113
+ // Owned bare Worlds use the native store on their volume, independent of the runtime.
114
+ const cmd = ['pnpm', 'add', '--save-dev', '--package-import-method=hardlink', '--config.nodeLinker=hoisted'];
112
115
  const r = spawnSync(cmd[0]!, [...cmd.slice(1), ...twins.map((t) => `@volter/twin-${t}`)], { windowsHide: true, cwd: dir, stdio: 'inherit', env: withoutVaultSignIn(process.env) });
113
116
  if (r.status !== 0) throw new Error(`installing the twins failed (${cmd.join(' ')} exited ${r.status})`);
114
117
  }