@volter/world 2.0.58 → 3.0.1

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/README.md CHANGED
@@ -22,6 +22,6 @@ console.log(world.log());
22
22
  await world.down();
23
23
  ```
24
24
 
25
- - [Getting started](https://github.com/volter-ai/twin/blob/main/docs/getting-started.md)
26
- - [CLI reference](https://github.com/volter-ai/twin/blob/main/docs/reference/cli.md)
27
- - [SDK reference](https://github.com/volter-ai/twin/blob/main/docs/reference/sdk.md)
25
+ - [Getting started](https://github.com/volter-ai/twin-world/blob/main/docs/getting-started.md)
26
+ - [CLI reference](https://github.com/volter-ai/twin-world/blob/main/docs/reference/cli.md)
27
+ - [SDK reference](https://github.com/volter-ai/twin-world/blob/main/docs/reference/sdk.md)
@@ -7,13 +7,13 @@ environment injection, a Node preload and a scoped HTTPS proxy route configured
7
7
  The state model is **Neon for every SaaS**: a log, checkpoints and branches over a parent position.
8
8
  Git-inspired verbs describe operations; there is no staging or commit step. Push moves entries
9
9
  between Worlds. Deploy performs them against a vendor at a real-system root under its policy.
10
- The canonical [model](https://github.com/volter-ai/twin/blob/main/docs/concepts/the-model.md),
11
- [lifecycle](https://github.com/volter-ai/twin/blob/main/docs/concepts/worlds.md) and
12
- [CLI reference](https://github.com/volter-ai/twin/blob/main/docs/reference/cli.md) own the details.
10
+ The canonical [model](https://github.com/volter-ai/twin-world/blob/main/docs/concepts/the-model.md),
11
+ [lifecycle](https://github.com/volter-ai/twin-world/blob/main/docs/concepts/worlds.md) and
12
+ [CLI reference](https://github.com/volter-ai/twin-world/blob/main/docs/reference/cli.md) own the details.
13
13
 
14
14
  ## Set up a World for an app
15
15
 
16
- In the app's folder (the [guide](https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md)):
16
+ In the app's folder (the [guide](https://github.com/volter-ai/twin-world/blob/main/docs/guides/use-with-a-coding-agent.md)):
17
17
 
18
18
  ```bash
19
19
  npm install -g @volter/world # once; `volter agents install` then adds this skill and the MCP server to your agents
@@ -10,5 +10,5 @@ operator instruction source shipped with this skill: setting a World up for an a
10
10
  to a platform, the `volter mcp` tools, and the lifecycle distinction between the product command
11
11
  `volter world` and the explicit operator command `volter-world`.
12
12
 
13
- The [documentation map](https://github.com/volter-ai/twin/blob/main/docs/README.md) links the
13
+ The [documentation map](https://github.com/volter-ai/twin-world/blob/main/docs/README.md) links the
14
14
  canonical model, task guides and command reference. Do not maintain a second copy here.
@@ -84,7 +84,7 @@ const writeJson = (path, value) => writeAtomic(path, `${JSON.stringify(value, nu
84
84
  /** A command run as itself; on Windows, where `claude` is a .cmd that runs only in cmd, one line to cmd with each argument
85
85
  * quoted (a path with a space stays one argument), as cross-spawn does. */
86
86
  const runTool = (bin, args) => process.platform === 'win32'
87
- ? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `"${[bin, ...args].map((a) => `"${a.replace(/"/g, '""')}"`).join(' ')}"`], { windowsHide: true, encoding: 'utf8', windowsVerbatimArguments: true })
87
+ ? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `"${[bin, ...args].map((a) => `"${a.replaceAll('"', '""')}"`).join(' ')}"`], { windowsHide: true, encoding: 'utf8', windowsVerbatimArguments: true })
88
88
  : spawnSync(bin, args, { windowsHide: true, encoding: 'utf8' });
89
89
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
90
90
  /** Our entry under `key` in a JSON config's server map: added, or already there; someone else's `volter` is left alone. */
package/dist/src/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--install] [--name <world>] [--allow-unknown] detect the app's vendors (installing their twins when none are), write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] [--origin <url>] serve this world on a URL under /<org>/<world>/ for apps; prints the token and the console (on loopback: view's front, no token in the browser)\n volter world view [--port <p>] [--host <h>] [--no-open] [--no-origins] [--origin <url>] step into this world in a browser: its twins' own UIs, timeline, clock, branches (a non-loopback --host serves them on one origin)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n volter twin <vendor> mirror [--port <p>] the twin's UI mirror\n volter twin <vendor> conformance check the twin against the vendor's spec\n\nagents \u2014 your coding agent, driving the same verbs\n volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here\n volter mcp the MCP server (stdio) an agent starts: the verbs above as tools\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> [--token <personal token>] sign the CLI into the hosted platform: approve it in the browser (or a token made under Account, for CI)\n volter whoami the platform the CLI is signed into, as whom, and its orgs\n volter logout revoke the CLI's token at the platform and forget it here\n volter remote add <name> <url|path|org/world> [--token <token>] [--create] name a remote; org/world (or world, in one org) resolves through the platform you logged into; --create makes it there from this world's twins\n volter open [<org>/<world>] [--no-open] a World's dashboard: a hosted one through the platform, else this world's (as view)\n volter completion bash|zsh|fish|powershell print a completion script for your shell\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n";
2
+ export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--install] [--name <world>] [--allow-unknown] detect the app's vendors (installing their twins when none are), write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] [--origin <url>] serve this world on a URL under /<org>/<world>/ for apps; prints the token and the console (on loopback: view's front, no token in the browser)\n volter world view [--port <p>] [--host <h>] [--no-open] [--no-origins] [--origin <url>] step into this world in a browser: its twins' own UIs, timeline, clock, branches (a non-loopback --host serves them on one origin)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved remote\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n\nagents \u2014 your coding agent, driving the same verbs\n volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here\n volter mcp the MCP server (stdio) an agent starts: the verbs above as tools\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> [--token <personal token>] sign the CLI into the hosted platform: approve it in the browser (or a token made under Account, for CI)\n volter whoami the platform the CLI is signed into, as whom, and its orgs\n volter logout revoke the CLI's token at the platform and forget it here\n volter remote add <name> <url|path|org/world> [--token <token>] [--create] name a remote; org/world (or world, in one org) resolves through the platform you logged into; --create makes it there from this world's twins\n volter open [<org>/<world>] [--no-open] a World's dashboard: a hosted one through the platform, else this world's (as view)\n volter completion bash|zsh|fish|powershell print a completion script for your shell\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n --version the command's version\n";
package/dist/src/cli.js CHANGED
@@ -64,7 +64,7 @@ world — the twins your app needs, running together (the world of the current
64
64
  volter world changeset [<name>] -m "<message>" cut the unpushed changes into a reviewable changeset
65
65
  volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)
66
66
  volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts
67
- volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror
67
+ volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved remote
68
68
  volter world verify <changeset> run the world's checks over a changeset and record the result
69
69
  volter world approve <changeset> --as <principal> sign a changeset's current hash
70
70
  volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy
@@ -75,8 +75,6 @@ twin — one twin: in this world, or on its own
75
75
  volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back
76
76
  volter twin <vendor> refresh observe the root now
77
77
  volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>
78
- volter twin <vendor> mirror [--port <p>] the twin's UI mirror
79
- volter twin <vendor> conformance check the twin against the vendor's spec
80
78
 
81
79
  agents — your coding agent, driving the same verbs
82
80
  volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here
@@ -96,6 +94,7 @@ remote — the worlds this one pushes to and fetches from, by name
96
94
  --world <dir> the world root, when the cwd is not inside it
97
95
  --branch <name> act on that branch instead of the checked-out one
98
96
  --json machine-readable output where offered
97
+ --version the command's version
99
98
  `;
100
99
  function flag(args, name) { return args.includes(name); }
101
100
  function value(args, name) {
@@ -236,6 +235,17 @@ async function worldCommand(verb, args) {
236
235
  const command = afterDashes(args);
237
236
  if (command.length === 0)
238
237
  throw new Error('volter world run: give the command after -- (volter world run -- npm test)');
238
+ // a stopped World is started first, as `volter world up` does (the default data the first time), and left running
239
+ let running = false;
240
+ try {
241
+ running = world.instance()?.running ?? false;
242
+ }
243
+ catch { /* never started */ }
244
+ if (!running) {
245
+ process.stderr.write(`${world.name} is not running: starting it (volter world up)
246
+ `);
247
+ await world.up({ seed: true });
248
+ }
239
249
  process.exitCode = await world.run(command, { verbose: flag(args.slice(0, args.indexOf('--')), '--verbose') });
240
250
  return;
241
251
  }
@@ -446,7 +456,7 @@ async function worldCommand(verb, args) {
446
456
  process.exit(0); // success follows confirmed shutdown; failures reach main's error path
447
457
  return;
448
458
  }
449
- // the console mounts when it is installed beside the CLI; without it the World serves its doors and mirrors
459
+ // the console mounts when it is installed beside the CLI; without it the World serves its doors and the twins
450
460
  const console = await (async () => {
451
461
  try {
452
462
  const mod = await import('@volter/world-console');
@@ -582,7 +592,7 @@ async function worldCommand(verb, args) {
582
592
  throw new Error(`volter world: unknown verb "${verb}"\n\n${HELP}`);
583
593
  }
584
594
  }
585
- /** `volter twin <vendor> <verb…>`: the twin's own CLI (serve, mirror, conformance), found the way a world finds it. */
595
+ /** `volter twin <vendor> <verb…>`: the twin's own CLI (serve), found the way a world finds it. */
586
596
  async function twinCommand(args) {
587
597
  const [vendor, ...rest] = args;
588
598
  if (!vendor || vendor.startsWith('-'))
@@ -620,7 +630,10 @@ async function twinCommand(args) {
620
630
  out(`${vendor} credential from ${found.from}`);
621
631
  input = found.input;
622
632
  }
623
- await world.sealTwinCredential(vendor, input);
633
+ const sealed = await world.sealTwinCredential(vendor, input);
634
+ // the root runbook: the ingest door's secret, shown once
635
+ if (sealed.mintedSigningSecret)
636
+ out(`${vendor} signing secret ${sealed.mintedSigningSecret} (enter it at the vendor as the webhook's secret; it is not shown again. A vendor that issues its own seals it instead: {"headers": {...}, "signingSecret": "<the vendor's>"})`);
624
637
  }
625
638
  else if (verb === 'refresh') {
626
639
  const r = await world.refreshTwin(vendor, { force: flag(rest, '--force') });
@@ -633,13 +646,16 @@ async function twinCommand(args) {
633
646
  return;
634
647
  }
635
648
  out(`${vendor} ${t.root ? `root ${t.root.url}${t.root.scope ? `/${t.root.scope}` : ''} deploy ${t.root.deploy}` : 'no root'} ${t.credential ? 'credential sealed' : 'no credential'}${t.url ? ` ${t.url}` : ''}`);
649
+ // the root runbook: where the vendor's webhooks go
650
+ if (t.root)
651
+ out(t.ingest ? `${vendor} ingest ${t.ingest} (register it at the vendor as the webhook URL, with the signing secret sealed with the credential)` : `${vendor} ingest: a served World's door (volter world serve, or a remote) is the webhook URL to register at the vendor`);
636
652
  return;
637
653
  }
638
654
  const dir = resolveCatalog(process.cwd()).packDir(vendor);
639
655
  if (dir === undefined)
640
656
  throw new Error(`No twin "${vendor}" here — \`bun add -d @volter/twin-${vendor}\`, or run from a checkout that has it`);
641
657
  if (rest.length === 0 || rest[0].startsWith('-'))
642
- throw new Error(`volter twin ${vendor}: which verb? serve | mirror | conformance`);
658
+ throw new Error(`volter twin ${vendor}: which verb? serve | root | credential | refresh`);
643
659
  const cli = packCli(dir);
644
660
  if (cli === undefined)
645
661
  throw new Error(`No cli for twin "${vendor}" (its manifest names no bin)`);
@@ -683,7 +699,12 @@ async function loginCommand(args) {
683
699
  throw new Error(`${origin} answered ${res.status}: ${body.error ?? 'not signed in'}`);
684
700
  storeToken(origin, token);
685
701
  storePlatform(origin, undefined, body.person?.email ?? body.person?.id);
686
- out(`signed in ${origin} as ${body.person?.email ?? body.person?.id ?? 'you'}\norgs ${(body.orgs ?? []).map((o) => o.slug ?? o.name).join(', ') || '(none yet)'}\nnext in your app's folder: volter world init, then volter remote add origin ${(body.orgs ?? [])[0]?.slug ?? '<org>'}/<world> --create`);
702
+ // a teammate's orgs already hold Worlds: the next step links one; a first person's makes one
703
+ const held = (body.orgs ?? []).flatMap((o) => (o.worlds ?? []).map((w) => w.name));
704
+ const next = held.length
705
+ ? `in your app's folder: volter remote add origin ${held[0]}, then volter world pull${held.length > 1 ? ` (your orgs' Worlds: ${held.slice(0, 6).join(', ')}${held.length > 6 ? ', …' : ''})` : ''}`
706
+ : `in your app's folder: volter world init, then volter remote add origin ${(body.orgs ?? [])[0]?.slug ?? '<org>'}/<world> --create`;
707
+ out(`signed in ${origin} as ${body.person?.email ?? body.person?.id ?? 'you'}\norgs ${(body.orgs ?? []).map((o) => o.slug ?? o.name).join(', ') || '(none yet)'}\nnext ${next}`);
687
708
  }
688
709
  /** `volter whoami`: which platform the command is signed into, as whom, and the orgs it reaches. */
689
710
  async function whoamiCommand() {
@@ -696,7 +717,7 @@ async function whoamiCommand() {
696
717
  }
697
718
  const res = await fetch(`${origin}/-/orgs`, { headers: { authorization: `Bearer ${token}` } }).catch(() => null);
698
719
  if (!res)
699
- throw new Error(`${origin} did not answer`);
720
+ throw new Error(`${origin} did not answer: the platform this command is signed into is unreachable (volter login <url> signs it into another)`);
700
721
  const body = (await res.json().catch(() => ({})));
701
722
  // a token the platform no longer takes is signed out; one it takes but that may not read orgs is still signed in
702
723
  if (res.status === 403) {
@@ -1023,6 +1044,10 @@ async function dispatch(noun, verb, args) {
1023
1044
  out(HELP);
1024
1045
  return;
1025
1046
  }
1047
+ if (noun === '--version' || noun === '-v' || noun === 'version') {
1048
+ out(`volter ${ownVersion()}`);
1049
+ return;
1050
+ }
1026
1051
  switch (noun) {
1027
1052
  case 'world': {
1028
1053
  if (!verb)
@@ -4,7 +4,7 @@
4
4
  // `World.open()` start here, the way `git` starts from the nearest `.git`.
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
6
6
  import { dirname, join, resolve } from 'node:path';
7
- import { stateDirName } from '@volter/world-core';
7
+ import { stateDirName } from '@volter/world-core/runtime';
8
8
  import { loadWorldConfig } from '@volter/world-runtime';
9
9
  export const WORLD_FILE = 'world.json';
10
10
  /** `<root>/.volter` */
package/dist/src/mcp.js CHANGED
@@ -78,7 +78,7 @@ Work in the app's folder (each tool takes "dir", defaulting to where this server
78
78
  - Look: world_log and world_diff say what the app changed at each vendor; world_view starts the dashboard and returns its link for the person.
79
79
  - Branch and reset like a database: world_branch, world_checkout, world_reset.
80
80
  - Share with a team: platform_login returns a link and a code the PERSON must approve in their browser (you cannot approve it); after they do, remote_link links this World to <org>/<world> on the platform (create: true makes it there from this World's vendors); world_changeset cuts the changes with a message, and world_push sends them.
81
- The guide: https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md`;
81
+ The guide: https://github.com/volter-ai/twin-world/blob/main/docs/guides/use-with-a-coding-agent.md`;
82
82
  export async function serveMcp(version) {
83
83
  const server = new McpServer({ name: 'volter', title: 'Volter World', version }, { instructions: INSTRUCTIONS });
84
84
  const here = (dir) => resolve(dir ?? process.cwd());
@@ -1,6 +1,6 @@
1
- import { rebaseChangeset, worldBootMarker, type Changeset, type LedgerDelta, type TwinAction, type WorldMarker, type ApplyReceipt, applyTwinWrite, type TwinResource } from '@volter/world-core';
1
+ import { rebaseChangeset, worldBootMarker, type Changeset, type LedgerDelta, type TwinAction, type WorldMarker, type ApplyReceipt, applyTwinWrite, type TwinResource } from '@volter/world-core/runtime';
2
2
  import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedWorld, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type ViewConsole, type WorldView, rootCredentialInput, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
3
- import { rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core';
3
+ import { rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
4
4
  export type WorldRef = {
5
5
  name?: string;
6
6
  root?: string;
@@ -53,7 +53,7 @@ export type WorldStatus = {
53
53
  };
54
54
  services: Record<string, {
55
55
  url?: string;
56
- running: boolean; /** the pack's protocol major and its standing (protocol 1 is deprecated: served under a warning, its vendor half throws) */
56
+ running: boolean; /** the pack's protocol major and its standing (the kernel serves its own major and refuses any other) */
57
57
  protocol?: {
58
58
  major: number;
59
59
  standing: string;
@@ -201,7 +201,7 @@ export declare class World {
201
201
  console: string | null;
202
202
  }) => void;
203
203
  }): Promise<ServedWorld>;
204
- /** `volter world view`: this World, its mirrors, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
204
+ /** `volter world view`: this World, its twins' screens, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
205
205
  view(opts?: {
206
206
  port?: number;
207
207
  host?: string;
@@ -224,6 +224,9 @@ export declare class World {
224
224
  url: string;
225
225
  namespace: string;
226
226
  } | null;
227
+ /** The origin the instance recorded, or null for a world that has never run (only world.json names it then);
228
+ * any other failure to read the instance is said as it is. */
229
+ private recordedOrigin;
227
230
  /** The origin this world clones from and pushes to, or null: the default data is its only origin. */
228
231
  origin(): WorldInstance['origin'] | null;
229
232
  /**
@@ -245,7 +248,8 @@ export declare class World {
245
248
  fetched: Awaited<ReturnType<typeof fetchFromOrigin>>;
246
249
  rebased: ReturnType<World['rebaseBranch']>;
247
250
  }>;
248
- /** Fetch what the origin observed since the last fetch. */
251
+ /** Fetch what the origin observed since the last fetch. A world that has never run (a teammate's checkout, linked
252
+ * with `remote add`) is booted as a clone boots it, with no story, and brings the origin's whole log in. */
249
253
  fetch(opts?: {
250
254
  token?: string;
251
255
  services?: string[];
@@ -284,6 +288,7 @@ export declare class World {
284
288
  twin(vendor: string): {
285
289
  vendor: string;
286
290
  url?: string;
291
+ ingest?: string;
287
292
  root: RootConfig | null;
288
293
  credential: {
289
294
  placedAt: string;
@@ -296,6 +301,7 @@ export declare class World {
296
301
  sealTwinCredential(vendor: string, input: string): Promise<{
297
302
  placedAt: string;
298
303
  fingerprint: string;
304
+ mintedSigningSecret?: string;
299
305
  }>;
300
306
  /** A root's credential when none is piped in: the signed-in vault, else the repo's `.env` (decision 0033). */
301
307
  rootCredentialInput(vendor: string): ReturnType<typeof rootCredentialInput>;
package/dist/src/world.js CHANGED
@@ -3,15 +3,15 @@
3
3
  // way git finds its repository; every method is a name for a world-runtime verb, in the words a
4
4
  // user reads. The `volter` command is one client of this class and adds nothing.
5
5
  import { platformPerson } from "./credentials.js";
6
- import { rebaseChangeset, worldBootMarker, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core';
6
+ import { rebaseChangeset, worldBootMarker, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core/runtime';
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import clockForm from '@volter/world-core/world-clock';
9
- import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn, materializeRoots, worldChangesetRules } from '@volter/world-runtime';
9
+ import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
10
10
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
11
  import { dirname, join, resolve } from 'node:path';
12
12
  import { requireToken, storeToken } from "./credentials.js";
13
13
  import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigRelative, worldEnvPath, worldSeedPath } from "./locate.js";
14
- import { destinationOf, parentEntries, rebaseBranch } from '@volter/world-core';
14
+ import { destinationOf, parentEntries, rebaseBranch } from '@volter/world-core/runtime';
15
15
  /** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
16
16
  /** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
17
17
  /** `30d`, `12h`, `5m`, `90s` in ms. */
@@ -170,7 +170,9 @@ export class World {
170
170
  branches: listWorlds(this.root).map((w) => w.name).sort(),
171
171
  running: instance?.running ?? false,
172
172
  served: (() => { const r = readServeRecord(this.root); return r ? { base: r.base, pid: r.pid, startedAt: r.startedAt } : null; })(),
173
- origin: instance?.origin ? { url: instance.origin.url, namespace: instance.origin.namespace, ...(instance.origin.fetchedAt ? { fetchedAt: instance.origin.fetchedAt } : {}) } : null,
173
+ // the origin push and pull use: the remote world.json names, with when it was last fetched (a world that only
174
+ // pushed to it has not fetched, but it has an origin all the same)
175
+ origin: (() => { const o = this.origin(); return o ? { url: o.url, namespace: o.namespace, ...(o.fetchedAt ? { fetchedAt: o.fetchedAt } : {}) } : null; })(),
174
176
  unpushed: instance ? this.unpushed().length : 0,
175
177
  changesets: { total: changesets.length, pushed: changesets.filter((c) => c.changeset.applied?.outcome === 'applied').length },
176
178
  services,
@@ -367,7 +369,7 @@ export class World {
367
369
  // ── serving ─────────────────────────────────────────────────────────────────────────────
368
370
  /** Serve this world on a port under `/<org>/<world>/`; returns when listening. */
369
371
  serve(opts = {}) { return serveWorld(this.name, { root: this.root, ...opts }); }
370
- /** `volter world view`: this World, its mirrors, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
372
+ /** `volter world view`: this World, its twins' screens, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
371
373
  view(opts = {}) { const person = platformPerson(); return serveWorldView(this.name, { root: this.root, ...(person ? { localPerson: person } : {}), ...opts }); }
372
374
  // ── remotes ─────────────────────────────────────────────────────────────────────────────
373
375
  /** The vendors this world has a twin of, as world.json names them (a twin service's package, else its id). */
@@ -402,12 +404,24 @@ export class World {
402
404
  return parseOriginUrl(target);
403
405
  if (name !== 'origin')
404
406
  return null;
405
- const recorded = worldOrigin(this.name, this.root);
407
+ const recorded = this.recordedOrigin();
406
408
  return recorded ? { url: recorded.url, namespace: recorded.namespace } : null;
407
409
  }
410
+ /** The origin the instance recorded, or null for a world that has never run (only world.json names it then);
411
+ * any other failure to read the instance is said as it is. */
412
+ recordedOrigin() {
413
+ try {
414
+ return worldOrigin(this.name, this.root);
415
+ }
416
+ catch (error) {
417
+ if (/World instance not found/.test(String(error.message)))
418
+ return null;
419
+ throw error;
420
+ }
421
+ }
408
422
  // ── the origin ──────────────────────────────────────────────────────────────────────────
409
423
  /** The origin this world clones from and pushes to, or null: the default data is its only origin. */
410
- origin() { const r = this.remote('origin'); const recorded = worldOrigin(this.name, this.root); return r ? { ...(recorded ?? {}), url: r.url, namespace: r.namespace } : recorded; }
424
+ origin() { const r = this.remote('origin'); const recorded = this.recordedOrigin(); return r ? { ...(recorded ?? {}), url: r.url, namespace: r.namespace } : recorded; }
411
425
  /**
412
426
  * Clone a remote's canonical history into this world: record the origin, remember the token,
413
427
  * fetch everything. With the world's token (namespace or read), the history arrives through the
@@ -436,14 +450,26 @@ export class World {
436
450
  const fetched = await this.fetch(opts);
437
451
  return { fetched, rebased: this.repos().map((r) => ({ service: r.service, ...rebaseBranch(r.stateService, r.root, { origin: true }) })) };
438
452
  }
439
- /** Fetch what the origin observed since the last fetch. */
440
- fetch(opts = {}) {
453
+ /** Fetch what the origin observed since the last fetch. A world that has never run (a teammate's checkout, linked
454
+ * with `remote add`) is booted as a clone boots it, with no story, and brings the origin's whole log in. */
455
+ async fetch(opts = {}) {
441
456
  const origin = this.origin();
442
457
  if (!origin)
443
458
  throw new Error(`World "${this.name}" has no origin — its only origin is the default data. \`volter world clone <url>\` connects a remote.`);
444
459
  if (opts.token !== undefined && opts.token !== '')
445
460
  storeToken(origin.url, opts.token); // the last token given for a remote is the one remembered
446
- return fetchFromOrigin(this.name, { root: this.root, url: origin.url, namespace: origin.namespace, key: requireToken(origin.url, opts.token), ...(opts.services ? { services: opts.services } : {}) });
461
+ const key = requireToken(origin.url, opts.token);
462
+ const instance = this.instance();
463
+ // state with no record is not a fresh checkout: a clean boot would take its unpushed work with it
464
+ if (!instance && existsSync(instanceDir(this.root, this.name)))
465
+ throw new Error(`World "${this.name}" has state but no instance record (${instanceDir(this.root, this.name)}): \`volter world up\` resumes it, or \`volter world down --purge\` starts it over`);
466
+ // a record missing a configured twin (a boot that failed part-way) is booted again, resuming what it holds
467
+ const configured = loadWorldConfig(this.configRef(), this.root).config.services.filter((s) => s.type === 'twin' || s.type === undefined).map((s) => s.id);
468
+ if (!instance || configured.some((id) => !(id in instance.services)))
469
+ await this.up({ seed: false });
470
+ // never fetched and nothing of its own: the origin's whole log, as a clone brings it
471
+ const full = !instance?.origin?.fetchedAt && this.log().length === 0;
472
+ return fetchFromOrigin(this.name, { root: this.root, url: origin.url, namespace: origin.namespace, key, ...(full ? { full: true } : {}), ...(opts.services ? { services: opts.services } : {}) });
447
473
  }
448
474
  // ── changesets and push ─────────────────────────────────────────────────────────────────
449
475
  /** Cut a changeset from the unpushed changes: the reviewable unit, with the author's message. */
@@ -503,7 +529,9 @@ export class World {
503
529
  if (!service)
504
530
  throw new Error(`no twin "${vendor}" in this world`);
505
531
  const inst = this.instance();
506
- return { vendor, ...(inst?.services[vendor]?.url ? { url: inst.services[vendor].url } : {}), root: service.root ?? null, credential: sealedCredentialInfo(this.root, vendor) };
532
+ // the door the vendor's webhooks are registered at: a served World's (architecture, "Ingest")
533
+ const served = readServeRecord(this.root);
534
+ return { vendor, ...(inst?.services[vendor]?.url ? { url: inst.services[vendor].url } : {}), ...(served ? { ingest: `${served.base.replace(/\/+$/, '')}/twins/${vendor}/ingest` } : {}), root: service.root ?? null, credential: sealedCredentialInfo(this.root, vendor) };
507
535
  }
508
536
  /** Set (or clear) the twin's root: the vendor's API and the deploy policy, in world.json. Takes effect at the next `up` or `serve`, or now when the world runs. */
509
537
  setTwinRoot(vendor, root) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world",
3
- "version": "2.0.58",
3
+ "version": "3.0.1",
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",
@@ -15,7 +15,7 @@
15
15
  "license": "Apache-2.0",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/volter-ai/twin.git",
18
+ "url": "git+https://github.com/volter-ai/twin-world.git",
19
19
  "directory": "packages/cli"
20
20
  },
21
21
  "publishConfig": {
@@ -30,12 +30,12 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.30.1",
33
- "@volter/world-core": "2.0.37",
34
- "@volter/world-runtime": "2.0.55",
33
+ "@volter/world-core": "3.0.1",
34
+ "@volter/world-runtime": "3.0.1",
35
35
  "zod": "^4.6.5"
36
36
  },
37
37
  "peerDependencies": {
38
- "@volter/world-console": "2.0.18"
38
+ "@volter/world-console": "3.0.1"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@volter/world-console": {
@@ -43,7 +43,7 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
- "@volter/world-console": "2.0.18"
46
+ "@volter/world-console": "3.0.1"
47
47
  },
48
48
  "bin": {
49
49
  "volter": "dist/src/cli.js"
@@ -7,13 +7,13 @@ environment injection, a Node preload and a scoped HTTPS proxy route configured
7
7
  The state model is **Neon for every SaaS**: a log, checkpoints and branches over a parent position.
8
8
  Git-inspired verbs describe operations; there is no staging or commit step. Push moves entries
9
9
  between Worlds. Deploy performs them against a vendor at a real-system root under its policy.
10
- The canonical [model](https://github.com/volter-ai/twin/blob/main/docs/concepts/the-model.md),
11
- [lifecycle](https://github.com/volter-ai/twin/blob/main/docs/concepts/worlds.md) and
12
- [CLI reference](https://github.com/volter-ai/twin/blob/main/docs/reference/cli.md) own the details.
10
+ The canonical [model](https://github.com/volter-ai/twin-world/blob/main/docs/concepts/the-model.md),
11
+ [lifecycle](https://github.com/volter-ai/twin-world/blob/main/docs/concepts/worlds.md) and
12
+ [CLI reference](https://github.com/volter-ai/twin-world/blob/main/docs/reference/cli.md) own the details.
13
13
 
14
14
  ## Set up a World for an app
15
15
 
16
- In the app's folder (the [guide](https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md)):
16
+ In the app's folder (the [guide](https://github.com/volter-ai/twin-world/blob/main/docs/guides/use-with-a-coding-agent.md)):
17
17
 
18
18
  ```bash
19
19
  npm install -g @volter/world # once; `volter agents install` then adds this skill and the MCP server to your agents
@@ -10,5 +10,5 @@ operator instruction source shipped with this skill: setting a World up for an a
10
10
  to a platform, the `volter mcp` tools, and the lifecycle distinction between the product command
11
11
  `volter world` and the explicit operator command `volter-world`.
12
12
 
13
- The [documentation map](https://github.com/volter-ai/twin/blob/main/docs/README.md) links the
13
+ The [documentation map](https://github.com/volter-ai/twin-world/blob/main/docs/README.md) links the
14
14
  canonical model, task guides and command reference. Do not maintain a second copy here.
package/src/agents.ts CHANGED
@@ -69,7 +69,7 @@ const writeJson = (path: string, value: unknown): void => writeAtomic(path, `${J
69
69
  /** A command run as itself; on Windows, where `claude` is a .cmd that runs only in cmd, one line to cmd with each argument
70
70
  * quoted (a path with a space stays one argument), as cross-spawn does. */
71
71
  const runTool = (bin: string, args: string[]): ReturnType<typeof spawnSync> => process.platform === 'win32'
72
- ? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `"${[bin, ...args].map((a) => `"${a.replace(/"/g, '""')}"`).join(' ')}"`], { windowsHide: true, encoding: 'utf8', windowsVerbatimArguments: true })
72
+ ? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `"${[bin, ...args].map((a) => `"${a.replaceAll('"', '""')}"`).join(' ')}"`], { windowsHide: true, encoding: 'utf8', windowsVerbatimArguments: true })
73
73
  : spawnSync(bin, args, { windowsHide: true, encoding: 'utf8' });
74
74
  const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
75
75
 
package/src/cli.ts CHANGED
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { spawn, spawnSync } from 'node:child_process';
10
10
  import { createHash } from 'node:crypto';
11
11
  import { hostname } from 'node:os';
12
- import { formatLedgerDelta } from '@volter/world-core';
12
+ import { formatLedgerDelta } from '@volter/world-core/runtime';
13
13
  import { existsSync } from 'node:fs';
14
14
  import { join, relative, resolve } from 'node:path';
15
15
  import { forgetPlatform, forgetToken, platformOrigin, requireToken, storePlatform, storeToken, tokenFor } from './credentials.ts';
@@ -56,7 +56,7 @@ world — the twins your app needs, running together (the world of the current
56
56
  volter world changeset [<name>] -m "<message>" cut the unpushed changes into a reviewable changeset
57
57
  volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)
58
58
  volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts
59
- volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror
59
+ volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved remote
60
60
  volter world verify <changeset> run the world's checks over a changeset and record the result
61
61
  volter world approve <changeset> --as <principal> sign a changeset's current hash
62
62
  volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy
@@ -67,8 +67,6 @@ twin — one twin: in this world, or on its own
67
67
  volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back
68
68
  volter twin <vendor> refresh observe the root now
69
69
  volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>
70
- volter twin <vendor> mirror [--port <p>] the twin's UI mirror
71
- volter twin <vendor> conformance check the twin against the vendor's spec
72
70
 
73
71
  agents — your coding agent, driving the same verbs
74
72
  volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here
@@ -88,6 +86,7 @@ remote — the worlds this one pushes to and fetches from, by name
88
86
  --world <dir> the world root, when the cwd is not inside it
89
87
  --branch <name> act on that branch instead of the checked-out one
90
88
  --json machine-readable output where offered
89
+ --version the command's version
91
90
  `;
92
91
 
93
92
  function flag(args: string[], name: string): boolean { return args.includes(name); }
@@ -197,6 +196,10 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
197
196
  case 'run': {
198
197
  const command = afterDashes(args);
199
198
  if (command.length === 0) throw new Error('volter world run: give the command after -- (volter world run -- npm test)');
199
+ // a stopped World is started first, as `volter world up` does (the default data the first time), and left running
200
+ let running = false; try { running = world.instance()?.running ?? false; } catch { /* never started */ }
201
+ if (!running) { process.stderr.write(`${world.name} is not running: starting it (volter world up)
202
+ `); await world.up({ seed: true }); }
200
203
  process.exitCode = await world.run(command, { verbose: flag(args.slice(0, args.indexOf('--')), '--verbose') });
201
204
  return;
202
205
  }
@@ -337,7 +340,7 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
337
340
  process.exit(0); // success follows confirmed shutdown; failures reach main's error path
338
341
  return;
339
342
  }
340
- // the console mounts when it is installed beside the CLI; without it the World serves its doors and mirrors
343
+ // the console mounts when it is installed beside the CLI; without it the World serves its doors and the twins
341
344
  const console = await (async (): Promise<ViewConsole | undefined> => {
342
345
  try { const mod = await import('@volter/world-console') as { createConsole: () => ViewConsole }; return mod.createConsole(); } catch (error) {
343
346
  // not installed is one thing; installed and broken is another, and says why
@@ -423,7 +426,7 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
423
426
  }
424
427
  }
425
428
 
426
- /** `volter twin <vendor> <verb…>`: the twin's own CLI (serve, mirror, conformance), found the way a world finds it. */
429
+ /** `volter twin <vendor> <verb…>`: the twin's own CLI (serve), found the way a world finds it. */
427
430
  async function twinCommand(args: string[]): Promise<void> {
428
431
  const [vendor, ...rest] = args;
429
432
  if (!vendor || vendor.startsWith('-')) throw new Error('volter twin: which twin? (volter twin stripe serve --port 12111)');
@@ -450,7 +453,9 @@ async function twinCommand(args: string[]): Promise<void> {
450
453
  out(`${vendor} credential from ${found.from}`);
451
454
  input = found.input;
452
455
  }
453
- await world.sealTwinCredential(vendor, input);
456
+ const sealed = await world.sealTwinCredential(vendor, input);
457
+ // the root runbook: the ingest door's secret, shown once
458
+ if (sealed.mintedSigningSecret) out(`${vendor} signing secret ${sealed.mintedSigningSecret} (enter it at the vendor as the webhook's secret; it is not shown again. A vendor that issues its own seals it instead: {"headers": {...}, "signingSecret": "<the vendor's>"})`);
454
459
  } else if (verb === 'refresh') {
455
460
  const r = await world.refreshTwin(vendor, { force: flag(rest, '--force') });
456
461
  out(r.refreshed ? `${vendor} refreshed ${r.report?.appended ?? 0} changed, ${r.report?.removed ?? 0} gone, ${r.report?.unchanged ?? 0} unchanged` : `${vendor} not refreshed: ${r.reason}`);
@@ -459,11 +464,13 @@ async function twinCommand(args: string[]): Promise<void> {
459
464
  const t = world.twin(vendor);
460
465
  if (flag(rest, '--json')) { json(t); return; }
461
466
  out(`${vendor} ${t.root ? `root ${t.root.url}${t.root.scope ? `/${t.root.scope}` : ''} deploy ${t.root.deploy}` : 'no root'} ${t.credential ? 'credential sealed' : 'no credential'}${t.url ? ` ${t.url}` : ''}`);
467
+ // the root runbook: where the vendor's webhooks go
468
+ if (t.root) out(t.ingest ? `${vendor} ingest ${t.ingest} (register it at the vendor as the webhook URL, with the signing secret sealed with the credential)` : `${vendor} ingest: a served World's door (volter world serve, or a remote) is the webhook URL to register at the vendor`);
462
469
  return;
463
470
  }
464
471
  const dir = resolveCatalog(process.cwd()).packDir(vendor);
465
472
  if (dir === undefined) throw new Error(`No twin "${vendor}" here — \`bun add -d @volter/twin-${vendor}\`, or run from a checkout that has it`);
466
- if (rest.length === 0 || rest[0]!.startsWith('-')) throw new Error(`volter twin ${vendor}: which verb? serve | mirror | conformance`);
473
+ if (rest.length === 0 || rest[0]!.startsWith('-')) throw new Error(`volter twin ${vendor}: which verb? serve | root | credential | refresh`);
467
474
  const cli = packCli(dir); if (cli === undefined) throw new Error(`No cli for twin "${vendor}" (its manifest names no bin)`);
468
475
  process.exitCode = await passthrough([process.execPath, cli, ...rest]);
469
476
  }
@@ -487,10 +494,15 @@ async function loginCommand(args: string[]): Promise<void> {
487
494
  let origin: string; try { const u = new URL(url); if (u.protocol !== 'https:' && u.protocol !== 'http:') throw new Error(); origin = u.origin; } catch { throw new Error(`${url} is not a platform address (https://…)`); }
488
495
  const token = value(args, '--token') ?? await deviceSignIn(origin, !flag(args, '--no-open'));
489
496
  const res = await fetch(`${origin}/-/orgs`, { headers: { authorization: `Bearer ${token}` } });
490
- const body = (await res.json().catch(() => ({}))) as { person?: { email?: string; id?: string }; orgs?: Array<{ slug: string | null; name: string }>; error?: string };
497
+ const body = (await res.json().catch(() => ({}))) as { person?: { email?: string; id?: string }; orgs?: Array<{ slug: string | null; name: string; worlds?: Array<{ name: string }> }>; error?: string };
491
498
  if (!res.ok) throw new Error(`${origin} answered ${res.status}: ${body.error ?? 'not signed in'}`);
492
499
  storeToken(origin, token); storePlatform(origin, undefined, body.person?.email ?? body.person?.id);
493
- out(`signed in ${origin} as ${body.person?.email ?? body.person?.id ?? 'you'}\norgs ${(body.orgs ?? []).map((o) => o.slug ?? o.name).join(', ') || '(none yet)'}\nnext in your app's folder: volter world init, then volter remote add origin ${(body.orgs ?? [])[0]?.slug ?? '<org>'}/<world> --create`);
500
+ // a teammate's orgs already hold Worlds: the next step links one; a first person's makes one
501
+ const held = (body.orgs ?? []).flatMap((o) => (o.worlds ?? []).map((w) => w.name));
502
+ const next = held.length
503
+ ? `in your app's folder: volter remote add origin ${held[0]}, then volter world pull${held.length > 1 ? ` (your orgs' Worlds: ${held.slice(0, 6).join(', ')}${held.length > 6 ? ', …' : ''})` : ''}`
504
+ : `in your app's folder: volter world init, then volter remote add origin ${(body.orgs ?? [])[0]?.slug ?? '<org>'}/<world> --create`;
505
+ out(`signed in ${origin} as ${body.person?.email ?? body.person?.id ?? 'you'}\norgs ${(body.orgs ?? []).map((o) => o.slug ?? o.name).join(', ') || '(none yet)'}\nnext ${next}`);
494
506
  }
495
507
 
496
508
  /** `volter whoami`: which platform the command is signed into, as whom, and the orgs it reaches. */
@@ -498,7 +510,7 @@ async function whoamiCommand(): Promise<void> {
498
510
  const origin = platformOrigin(); const token = origin ? tokenFor(origin) : undefined;
499
511
  if (!origin || !token) { out('not signed in (volter login <platform url>)'); process.exitCode = 1; return; }
500
512
  const res = await fetch(`${origin}/-/orgs`, { headers: { authorization: `Bearer ${token}` } }).catch(() => null);
501
- if (!res) throw new Error(`${origin} did not answer`);
513
+ if (!res) throw new Error(`${origin} did not answer: the platform this command is signed into is unreachable (volter login <url> signs it into another)`);
502
514
  const body = (await res.json().catch(() => ({}))) as { person?: { email?: string; id?: string }; orgs?: Array<{ slug: string | null; name: string; role?: string }>; error?: string };
503
515
  // a token the platform no longer takes is signed out; one it takes but that may not read orgs is still signed in
504
516
  if (res.status === 403) { out(`signed in ${origin} (this token may not read your orgs: ${body.error ?? 'refused'})`); return; }
@@ -749,6 +761,7 @@ async function main(): Promise<void> {
749
761
  async function dispatch(noun: string | undefined, verb: string | undefined, args: string[]): Promise<void> {
750
762
  if (!noun || noun === '--help' || noun === '-h' || noun === 'help') { out(HELP); return; }
751
763
  if (verb === '--help' || verb === '-h') { out(HELP); return; }
764
+ if (noun === '--version' || noun === '-v' || noun === 'version') { out(`volter ${ownVersion()}`); return; }
752
765
  switch (noun) {
753
766
  case 'world': {
754
767
  if (!verb) throw new Error(`volter world: which verb?\n\n${HELP}`);
@@ -2,14 +2,15 @@
2
2
  // #the-config-and-the-running-world): init copies a twin's starter handlers to .volter/handlers/<vendor>.json and
3
3
  // names that path relative to the world root, so the committed config reads the same on every
4
4
  // machine; `up` resolves it against the root on both the spawn path (--scenario) and the colocated
5
- // host (scenarioPath). Proven against the real anthropic twin, which ships starter handlers.
5
+ // host (scenarioPath). Proven against the real anthropic twin, installed with starter handlers of the test's own (no
6
+ // pack of the catalog ships any): its files linked from the pack repository, its `defaults/handlers.json` written here.
6
7
  import { afterAll, describe, expect, test } from 'bun:test';
7
- import { mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
8
+ import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
8
9
  import { tmpdir } from 'node:os';
9
10
  import { join, resolve } from 'node:path';
10
11
  import { World } from './index.ts';
11
12
 
12
- const PACKS = resolve(import.meta.dir, '..', '..', 'twin');
13
+ const PACKS = resolve(import.meta.dir, '..', '..', '..', '..', 'twin-packs-p3');
13
14
 
14
15
  function app(): string {
15
16
  const parent = mkdtempSync(join(tmpdir(), 'handlers-in-repo-'));
@@ -17,8 +18,14 @@ function app(): string {
17
18
  mkdirSync(dir);
18
19
  writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'agent', dependencies: { '@anthropic-ai/sdk': '^0.30' } }));
19
20
  writeFileSync(join(dir, '.env.example'), 'ANTHROPIC_API_KEY=\n');
20
- mkdirSync(join(parent, 'node_modules', '@volter'), { recursive: true });
21
- symlinkSync(join(PACKS, 'anthropic'), join(parent, 'node_modules', '@volter', 'twin-anthropic'));
21
+ const installed = join(parent, 'node_modules', '@volter', 'twin-anthropic');
22
+ mkdirSync(installed, { recursive: true });
23
+ for (const entry of readdirSync(join(PACKS, 'anthropic'))) {
24
+ if (entry === 'package.json') copyFileSync(join(PACKS, 'anthropic', entry), join(installed, entry));
25
+ else symlinkSync(join(PACKS, 'anthropic', entry), join(installed, entry));
26
+ }
27
+ mkdirSync(join(installed, 'defaults'));
28
+ writeFileSync(join(installed, 'defaults', 'handlers.json'), `${JSON.stringify({ handlers: [{ id: 'demo-answer', on: {}, respond: { text: 'hi' } }] }, null, 2)}\n`);
22
29
  return dir;
23
30
  }
24
31
 
@@ -6,7 +6,7 @@ import { resolve } from 'node:path';
6
6
  export const CLI = resolve(import.meta.dir, '..', 'cli.ts');
7
7
  /** The operator's `volter-world` command, which ships with @volter/world-runtime. */
8
8
  export const OPERATOR_CLI = resolve(import.meta.dir, '..', '..', '..', 'world-runtime', 'src', 'cli.ts');
9
- /** The twins, as this checkout holds them. */
10
- export const PACKS = resolve(import.meta.dir, '..', '..', '..', 'twin');
9
+ /** The twins: the pack repository beside this checkout. */
10
+ export const PACKS = resolve(import.meta.dir, '..', '..', '..', '..', '..', 'twin-packs-p3');
11
11
  /** @volter/world itself. */
12
12
  export const SDK = resolve(import.meta.dir, '..', '..');
@@ -17,7 +17,7 @@
17
17
  //
18
18
  // The same steps make the recording: `tapeFor(page)` renders a vhs tape that types each command,
19
19
  // waits, and screenshots after each one, so docs/media/<page>/ is the page's own playback.
20
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
20
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
21
21
  import { downWorld } from '@volter/world-runtime';
22
22
  import { tmpdir } from 'node:os';
23
23
  import { basename, delimiter, dirname, join, resolve } from 'node:path';
@@ -108,7 +108,9 @@ const globalPath = (global: string): string => [...(process.platform === 'win32'
108
108
  * package.json, its env example, its files — and installs what it names.
109
109
  */
110
110
  export function tutorialApp(name: string, registry: { env: Record<string, string> }): { dir: string; parent: string; env: Record<string, string> } {
111
- const parent = mkdtempSync(join(tmpdir(), `tutorial-${name}-`));
111
+ // the real path: macOS's temp directory is a symlink (/var → /private/var), and a World is known by its root's real
112
+ // path, so the harness and the CLI the page runs name it alike
113
+ const parent = realpathSync(mkdtempSync(join(tmpdir(), `tutorial-${name}-`)));
112
114
  const dir = join(parent, 'acme-web');
113
115
  mkdirSync(dir);
114
116
  const global = join(parent, 'bun-global');
@@ -227,6 +229,7 @@ export async function runTutorial(tutorial: Tutorial, app: { dir: string; parent
227
229
  };
228
230
  const fenceOutput = new Map<number, string>(); // what the reader saw after each bash fence, all its commands
229
231
  const backgroundFences = new Set<number>();
232
+ let leak: Error | undefined;
230
233
  try {
231
234
  for (const step of tutorial.steps) {
232
235
  if (step.kind === 'file') {
@@ -282,9 +285,13 @@ export async function runTutorial(tutorial: Tutorial, app: { dir: string; parent
282
285
  const alive = new Set(windowsProcesses().map((p) => `${p.pid}@${p.created}`));
283
286
  for (const p of leftovers) if (alive.has(`${p.pid}@${p.created}`)) Bun.spawnSync({ cmd: ['taskkill', '/PID', String(p.pid), '/T', '/F'], windowsHide: true, stdout: 'ignore', stderr: 'ignore' });
284
287
  await Promise.allSettled([reader.cancel(), errReader.cancel()]);
285
- throw new Error(`the page left ${leftovers.length} process(es) running after its own stop and the harness's kill of its jobs (ended now):\n${leftovers.map((p) => ` pid ${p.pid}: ${p.command}`).join('\n')}`);
288
+ // not thrown here: a throw in `finally` replaces the page's own failure with this report. It is thrown after,
289
+ // when the page itself passed; when the page failed, its failure propagates and this is printed beside it
290
+ leak = new Error(`the page left ${leftovers.length} process(es) running after its own stop and the harness's kill of its jobs (ended now):\n${leftovers.map((p) => ` pid ${p.pid}: ${p.command}`).join('\n')}`);
291
+ console.error(leak.message);
286
292
  }
287
293
  }
294
+ if (leak) throw leak;
288
295
  return results;
289
296
  }
290
297
 
package/src/locate.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // `World.open()` start here, the way `git` starts from the nearest `.git`.
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
6
6
  import { dirname, join, resolve } from 'node:path';
7
- import { stateDirName } from '@volter/world-core';
7
+ import { stateDirName } from '@volter/world-core/runtime';
8
8
  import { loadWorldConfig } from '@volter/world-runtime';
9
9
 
10
10
  export const WORLD_FILE = 'world.json';
package/src/mcp.ts CHANGED
@@ -72,7 +72,7 @@ Work in the app's folder (each tool takes "dir", defaulting to where this server
72
72
  - Look: world_log and world_diff say what the app changed at each vendor; world_view starts the dashboard and returns its link for the person.
73
73
  - Branch and reset like a database: world_branch, world_checkout, world_reset.
74
74
  - Share with a team: platform_login returns a link and a code the PERSON must approve in their browser (you cannot approve it); after they do, remote_link links this World to <org>/<world> on the platform (create: true makes it there from this World's vendors); world_changeset cuts the changes with a message, and world_push sends them.
75
- The guide: https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md`;
75
+ The guide: https://github.com/volter-ai/twin-world/blob/main/docs/guides/use-with-a-coding-agent.md`;
76
76
 
77
77
  export async function serveMcp(version: string): Promise<void> {
78
78
  const server = new McpServer({ name: 'volter', title: 'Volter World', version }, { instructions: INSTRUCTIONS });
package/src/sdk.test.ts CHANGED
@@ -53,7 +53,7 @@ describe('World — the twins an app needs, running together', () => {
53
53
  // the default data is the only origin: nothing to push to, nothing to fetch from
54
54
  expect(world.origin()).toBeNull();
55
55
  await expect(world.push()).rejects.toThrow(/default data cannot be pushed to/);
56
- expect(() => world.fetch()).toThrow(/only origin is the default data/);
56
+ await expect(world.fetch()).rejects.toThrow(/only origin is the default data/);
57
57
 
58
58
  // a branch over the same mirrors with its own log, checked out; checkout switches back
59
59
  const feature = await world.branch('feature');
@@ -74,7 +74,7 @@ describe('World — the twins an app needs, running together', () => {
74
74
  expect(back.status().running).toBe(true);
75
75
  expect(back.unpushed()).toHaveLength(1);
76
76
  await back.down();
77
- });
77
+ }, 30_000); // two Worlds booted and one resumed: its one-service skeleton alone took 2.4 s (load ~13, 2026-09-30, a scratch probe of up/branch/checkout/down/up/down)
78
78
 
79
79
  test('a remote URL names one world; tokens live in the user\'s config dir, never in the repo', () => {
80
80
  expect(parseOriginUrl('https://twins.example.com/acme/web')).toEqual({ url: 'https://twins.example.com', namespace: 'acme/web' });
package/src/world.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // way git finds its repository; every method is a name for a world-runtime verb, in the words a
4
4
  // user reads. The `volter` command is one client of this class and adds nothing.
5
5
  import { platformPerson } from './credentials.ts';
6
- import { rebaseChangeset, worldBootMarker, type Changeset, type ChangesetApplication, type LedgerDelta, type PerformAction, type RemoteExecute, type TwinAction, type WorldMarker, type ApplyReceipt, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources, type TwinResource } from '@volter/world-core';
6
+ import { rebaseChangeset, worldBootMarker, type Changeset, type ChangesetApplication, type LedgerDelta, type PerformAction, type RemoteExecute, type TwinAction, type WorldMarker, type ApplyReceipt, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources, type TwinResource } from '@volter/world-core/runtime';
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import clockForm from '@volter/world-core/world-clock';
9
9
  import {
@@ -11,12 +11,12 @@ import {
11
11
  statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
12
12
  bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, type ServedWorld, type ViewConsole, type WorldView, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn,
13
13
  type DeployTwinOutcome, type MaterializedRoot,
14
- type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance, materializeRoots, worldChangesetRules } from '@volter/world-runtime';
14
+ type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
15
15
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
16
16
  import { dirname, join, resolve } from 'node:path';
17
17
  import { requireToken, storeToken } from './credentials.ts';
18
18
  import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigRelative, worldEnvPath, worldSeedPath } from './locate.ts';
19
- import { destinationOf, parentEntries, rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core';
19
+ import { destinationOf, parentEntries, rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
20
20
 
21
21
  export type WorldRef = { name?: string; root?: string };
22
22
  export type WorldMode = 'local' | 'share' | 'sealed';
@@ -56,7 +56,7 @@ export type WorldStatus = {
56
56
  origin: { url: string; namespace: string; fetchedAt?: string } | null;
57
57
  unpushed: number;
58
58
  changesets: { total: number; pushed: number };
59
- services: Record<string, { url?: string; running: boolean; /** the pack's protocol major and its standing (protocol 1 is deprecated: served under a warning, its vendor half throws) */ protocol?: { major: number; standing: string } }>;
59
+ services: Record<string, { url?: string; running: boolean; /** the pack's protocol major and its standing (the kernel serves its own major and refuses any other) */ protocol?: { major: number; standing: string } }>;
60
60
  envFile?: string;
61
61
  };
62
62
 
@@ -175,7 +175,9 @@ export class World {
175
175
  branches: listWorlds(this.root).map((w) => w.name).sort(),
176
176
  running: instance?.running ?? false,
177
177
  served: (() => { const r = readServeRecord(this.root); return r ? { base: r.base, pid: r.pid, startedAt: r.startedAt } : null; })(),
178
- origin: instance?.origin ? { url: instance.origin.url, namespace: instance.origin.namespace, ...(instance.origin.fetchedAt ? { fetchedAt: instance.origin.fetchedAt } : {}) } : null,
178
+ // the origin push and pull use: the remote world.json names, with when it was last fetched (a world that only
179
+ // pushed to it has not fetched, but it has an origin all the same)
180
+ origin: (() => { const o = this.origin(); return o ? { url: o.url, namespace: o.namespace, ...(o.fetchedAt ? { fetchedAt: o.fetchedAt } : {}) } : null; })(),
179
181
  unpushed: instance ? this.unpushed().length : 0,
180
182
  changesets: { total: changesets.length, pushed: changesets.filter((c) => c.changeset.applied?.outcome === 'applied').length },
181
183
  services,
@@ -338,7 +340,7 @@ export class World {
338
340
  // ── serving ─────────────────────────────────────────────────────────────────────────────
339
341
  /** Serve this world on a port under `/<org>/<world>/`; returns when listening. */
340
342
  serve(opts: { port?: number; host?: string; consolePort?: number; announce?: (info: { name: string; base: string; token: string; readToken: string; console: string | null }) => void } = {}): Promise<ServedWorld> { return serveWorld(this.name, { root: this.root, ...opts }); }
341
- /** `volter world view`: this World, its mirrors, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
343
+ /** `volter world view`: this World, its twins' screens, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
342
344
  view(opts: { port?: number; host?: string; console?: ViewConsole; announce?: (line: string) => void; origins?: boolean; origin?: string } = {}): Promise<WorldView> { const person = platformPerson(); return serveWorldView(this.name, { root: this.root, ...(person ? { localPerson: person } : {}), ...opts }); }
343
345
 
344
346
  // ── remotes ─────────────────────────────────────────────────────────────────────────────
@@ -369,13 +371,18 @@ export class World {
369
371
  const target = this.remotes()[name];
370
372
  if (target) return parseOriginUrl(target);
371
373
  if (name !== 'origin') return null;
372
- const recorded = worldOrigin(this.name, this.root);
374
+ const recorded = this.recordedOrigin();
373
375
  return recorded ? { url: recorded.url, namespace: recorded.namespace } : null;
374
376
  }
375
377
 
378
+ /** The origin the instance recorded, or null for a world that has never run (only world.json names it then);
379
+ * any other failure to read the instance is said as it is. */
380
+ private recordedOrigin(): WorldInstance['origin'] | null {
381
+ try { return worldOrigin(this.name, this.root); } catch (error) { if (/World instance not found/.test(String((error as Error).message))) return null; throw error; }
382
+ }
376
383
  // ── the origin ──────────────────────────────────────────────────────────────────────────
377
384
  /** The origin this world clones from and pushes to, or null: the default data is its only origin. */
378
- origin(): WorldInstance['origin'] | null { const r = this.remote('origin'); const recorded = worldOrigin(this.name, this.root); return r ? { ...(recorded ?? {}), url: r.url, namespace: r.namespace } : recorded; }
385
+ origin(): WorldInstance['origin'] | null { const r = this.remote('origin'); const recorded = this.recordedOrigin(); return r ? { ...(recorded ?? {}), url: r.url, namespace: r.namespace } : recorded; }
379
386
  /**
380
387
  * Clone a remote's canonical history into this world: record the origin, remember the token,
381
388
  * fetch everything. With the world's token (namespace or read), the history arrives through the
@@ -401,12 +408,22 @@ export class World {
401
408
  const fetched = await this.fetch(opts);
402
409
  return { fetched, rebased: this.repos().map((r) => ({ service: r.service, ...rebaseBranch(r.stateService, r.root, { origin: true }) })) };
403
410
  }
404
- /** Fetch what the origin observed since the last fetch. */
405
- fetch(opts: { token?: string; services?: string[] } = {}): ReturnType<typeof fetchFromOrigin> {
411
+ /** Fetch what the origin observed since the last fetch. A world that has never run (a teammate's checkout, linked
412
+ * with `remote add`) is booted as a clone boots it, with no story, and brings the origin's whole log in. */
413
+ async fetch(opts: { token?: string; services?: string[] } = {}): ReturnType<typeof fetchFromOrigin> {
406
414
  const origin = this.origin();
407
415
  if (!origin) throw new Error(`World "${this.name}" has no origin — its only origin is the default data. \`volter world clone <url>\` connects a remote.`);
408
416
  if (opts.token !== undefined && opts.token !== '') storeToken(origin.url, opts.token); // the last token given for a remote is the one remembered
409
- return fetchFromOrigin(this.name, { root: this.root, url: origin.url, namespace: origin.namespace, key: requireToken(origin.url, opts.token), ...(opts.services ? { services: opts.services } : {}) });
417
+ const key = requireToken(origin.url, opts.token);
418
+ const instance = this.instance();
419
+ // state with no record is not a fresh checkout: a clean boot would take its unpushed work with it
420
+ if (!instance && existsSync(instanceDir(this.root, this.name))) throw new Error(`World "${this.name}" has state but no instance record (${instanceDir(this.root, this.name)}): \`volter world up\` resumes it, or \`volter world down --purge\` starts it over`);
421
+ // a record missing a configured twin (a boot that failed part-way) is booted again, resuming what it holds
422
+ const configured = loadWorldConfig(this.configRef(), this.root).config.services.filter((s) => s.type === 'twin' || s.type === undefined).map((s) => s.id);
423
+ if (!instance || configured.some((id) => !(id in instance.services))) await this.up({ seed: false });
424
+ // never fetched and nothing of its own: the origin's whole log, as a clone brings it
425
+ const full = !instance?.origin?.fetchedAt && this.log().length === 0;
426
+ return fetchFromOrigin(this.name, { root: this.root, url: origin.url, namespace: origin.namespace, key, ...(full ? { full: true } : {}), ...(opts.services ? { services: opts.services } : {}) });
410
427
  }
411
428
 
412
429
  // ── changesets and push ─────────────────────────────────────────────────────────────────
@@ -456,12 +473,14 @@ export class World {
456
473
  rebase(name: string): ReturnType<typeof rebaseWorldChangeset> { return rebaseWorldChangeset(name, { root: this.root, world: this.name }); }
457
474
  // ── roots (docs/concepts/the-model.md: a twin on a shared world whose root is the vendor) ──
458
475
  /** The twin's root and credential, as `volter twin <vendor>` prints them. */
459
- twin(vendor: string): { vendor: string; url?: string; root: RootConfig | null; credential: { placedAt: string; fingerprint: string } | null } {
476
+ twin(vendor: string): { vendor: string; url?: string; ingest?: string; root: RootConfig | null; credential: { placedAt: string; fingerprint: string } | null } {
460
477
  const { config } = loadWorldConfig(this.configRef(), this.root);
461
478
  const service = config.services.find((s) => s.id === vendor);
462
479
  if (!service) throw new Error(`no twin "${vendor}" in this world`);
463
480
  const inst = this.instance();
464
- return { vendor, ...(inst?.services[vendor]?.url ? { url: inst.services[vendor]!.url } : {}), root: (service as { root?: RootConfig }).root ?? null, credential: sealedCredentialInfo(this.root, vendor) };
481
+ // the door the vendor's webhooks are registered at: a served World's (architecture, "Ingest")
482
+ const served = readServeRecord(this.root);
483
+ return { vendor, ...(inst?.services[vendor]?.url ? { url: inst.services[vendor]!.url } : {}), ...(served ? { ingest: `${served.base.replace(/\/+$/, '')}/twins/${vendor}/ingest` } : {}), root: (service as { root?: RootConfig }).root ?? null, credential: sealedCredentialInfo(this.root, vendor) };
465
484
  }
466
485
  /** Set (or clear) the twin's root: the vendor's API and the deploy policy, in world.json. Takes effect at the next `up` or `serve`, or now when the world runs. */
467
486
  setTwinRoot(vendor: string, root: RootConfig | null): void {
@@ -470,7 +489,7 @@ export class World {
470
489
  if (inst?.running) { materializeRoots(loadWorldConfig(this.configRef(), this.root).config, inst.dirs.data, this.root); }
471
490
  }
472
491
  /** Seal the vendor's credential beside the world (a bare token, or a JSON payload). Never readable back. */
473
- sealTwinCredential(vendor: string, input: string): Promise<{ placedAt: string; fingerprint: string }> {
492
+ sealTwinCredential(vendor: string, input: string): Promise<{ placedAt: string; fingerprint: string; mintedSigningSecret?: string }> {
474
493
  // sealed for the one destination it serves, the twin's root: with none set there is nowhere it may go
475
494
  const url = (loadWorldConfig(this.configRef(), this.root).config.services as Array<{ id?: string; root?: { url?: string } }>).find((s) => s.id === vendor)?.root?.url;
476
495
  if (!url) return Promise.reject(new Error(`the ${vendor} twin has no root: \`volter twin ${vendor} root <url>\` sets where its credential goes, then seal it`));