@volter/world 3.0.193 → 3.0.194

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.
@@ -13,7 +13,7 @@ You need Node 22.6+, pnpm 12.15.0, Docker Compose and registry access during ins
13
13
  Copy this starter into a new folder:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.193 example customer-onboarding --dir onboarding
16
+ $ npx @volter/world@3.0.194 example customer-onboarding --dir onboarding
17
17
  $ cd onboarding
18
18
  $ pnpm install
19
19
  ```
@@ -7,22 +7,22 @@ try {
7
7
  await initialize();
8
8
  switch (process.argv[2]) {
9
9
  case 'baseline': {
10
- world.setClock(at);
10
+ await world.setClock(at);
11
11
  const rows = await listSignups();
12
12
  if (rows.length > 1 || rows.some(r => r.name !== 'Grace')) throw new Error('This demo contains customer work; reset both your disposable application database and demo World explicitly.');
13
13
  const grace = rows.find(r => r.name === 'Grace') ?? await onboard({ name: 'Grace', email: 'owner@world.test' });
14
- world.advanceClock('5s');
14
+ await world.advanceClock('5s');
15
15
  console.log('Baseline', JSON.stringify(summarize((await listSignups()).find(r => r.customer_id === grace.customer_id)))); break;
16
16
  }
17
17
  case 'present': {
18
18
  const rows = await listSignups();
19
19
  if (rows.length !== 1 || rows[0].name !== 'Grace') throw new Error('Prepare the Grace-only baseline first.');
20
- world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
20
+ await world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
21
21
  const partial = await onboard({ name: 'Ada', email: 'owner@world.test', from: 'Example <welcome@unverified.world.test>' });
22
22
  console.log('Email refusal; customer and SQL row retained', JSON.stringify(summarize(partial)));
23
23
  const retry = await retryWelcome(partial.customer_id);
24
24
  console.log('Retry accepted on the same customer', JSON.stringify(summarize(retry)));
25
- world.advanceClock('5s');
25
+ await world.advanceClock('5s');
26
26
  console.log('Delivered', JSON.stringify((await listSignups()).map(summarize))); break;
27
27
  }
28
28
  default: throw new Error('Use baseline or present');
@@ -11,13 +11,13 @@
11
11
  "pg": "8.16.3"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.193",
15
- "@volter/world-core": "3.0.166",
16
- "@volter/world-runtime": "3.0.191",
17
- "@volter/world-console": "3.0.161",
14
+ "@volter/world": "3.0.194",
15
+ "@volter/world-core": "3.0.167",
16
+ "@volter/world-runtime": "3.0.192",
17
+ "@volter/world-console": "3.0.162",
18
18
  "@volter/twin-stripe": "3.0.2",
19
19
  "@volter/twin-resend": "1.0.1",
20
- "@volter/world-ui": "1.0.165"
20
+ "@volter/world-ui": "1.0.166"
21
21
  },
22
22
  "packageManager": "pnpm@12.15.0",
23
23
  "devEngines": {
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
13
13
  Use an empty parent folder. Create the baseline starter there:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.193 example integration-branches --dir baseline
16
+ $ npx @volter/world@3.0.194 example integration-branches --dir baseline
17
17
  $ cd baseline
18
18
  $ pnpm install
19
19
  $ ./node_modules/.bin/volter world init --name baseline --twins stripe
@@ -38,9 +38,9 @@ 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.193 example integration-branches --dir integration
42
- $ npx @volter/world@3.0.193 example integration-branches --dir worker-a
43
- $ npx @volter/world@3.0.193 example integration-branches --dir worker-b
41
+ $ npx @volter/world@3.0.194 example integration-branches --dir integration
42
+ $ npx @volter/world@3.0.194 example integration-branches --dir worker-a
43
+ $ npx @volter/world@3.0.194 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:
@@ -9,12 +9,12 @@
9
9
  "stripe": "17.7.0"
10
10
  },
11
11
  "devDependencies": {
12
- "@volter/world": "3.0.193",
13
- "@volter/world-core": "3.0.166",
14
- "@volter/world-runtime": "3.0.191",
15
- "@volter/world-console": "3.0.161",
12
+ "@volter/world": "3.0.194",
13
+ "@volter/world-core": "3.0.167",
14
+ "@volter/world-runtime": "3.0.192",
15
+ "@volter/world-console": "3.0.162",
16
16
  "@volter/twin-stripe": "3.0.2",
17
- "@volter/world-ui": "1.0.165"
17
+ "@volter/world-ui": "1.0.166"
18
18
  },
19
19
  "packageManager": "pnpm@12.15.0",
20
20
  "devEngines": {
@@ -19,7 +19,7 @@ The catalog offers current defaults. Keep this example's manifest and installati
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.193 example workplace-agent --dir workplace
22
+ $ npx @volter/world@3.0.194 example workplace-agent --dir workplace
23
23
  $ cd workplace
24
24
  $ pnpm install
25
25
  $ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
@@ -11,14 +11,14 @@
11
11
  "@slack/web-api": "7.19.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.193",
15
- "@volter/world-core": "3.0.166",
16
- "@volter/world-runtime": "3.0.191",
17
- "@volter/world-console": "3.0.161",
14
+ "@volter/world": "3.0.194",
15
+ "@volter/world-core": "3.0.167",
16
+ "@volter/world-runtime": "3.0.192",
17
+ "@volter/world-console": "3.0.162",
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.165"
21
+ "@volter/world-ui": "1.0.166"
22
22
  },
23
23
  "packageManager": "pnpm@12.15.0",
24
24
  "devEngines": {
@@ -117,10 +117,12 @@ Identity comes from credentials in the vendor's manner; inspect `GET /twin` for
117
117
  identity and time rules. Use `clock set` and `clock advance` for backdated history. Without a
118
118
  frozen World clock, the runtime uses wall time.
119
119
 
120
- Author judgment, stateless lookups and faults in `handlers/<vendor>.json` in the World config
121
- directory (typically `.volter/handlers/`). These are ordered, first-match scenario rules.
122
- Edit the file and restart to load changes; a handler must never fake success for a stored
123
- mutation. `GET /twin/scenario` shows handler matches and misses; `tail` records misses to author.
120
+ Place a vendor's judgment (a decision) and failures (faults) on a scenario's timeline,
121
+ `scenarios/<name>.json` beside `world.json` (typically `.volter/scenarios/`), and run it with
122
+ `volter world play <file>`: it resets to the default data, sets the clock to the scenario's
123
+ `start` and makes the scenario the World's, which every twin reads at each request. A twin turns
124
+ each input into its own answer and records it; nothing in a scenario answers in its place.
125
+ `GET /twin/scenario` shows what is placed on that twin and what it consumed.
124
126
  Model twins serve deterministic scripts or labeled stubs, so verify calls and state changes,
125
127
  not the quality of prose from an unconfigured model.
126
128
 
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 a shared simulated internet (the world of the current directory)\n volter world init [--install] [--name <world>] [--twins a,b] [--source vendor=package] [--allow-unknown] detect or select vendors, choose installed publishers, 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 env [--template <file>] render deployment .env.world references into dotenv files\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 example list ready application starters included in this CLI release\n volter example <name> [--dir <path>] copy a cookbook starter into an empty directory; does not install or start it\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 generate write seed.generated.ts from world.json's people, organizations and accounts\n volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)\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>] [--remote <remote>] [--token <token>] [--force] send entries to the selected remote (default origin); vendor deployment follows its root policy\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 submit --source <id> --vendor <vendor> --package <name> --version <exact> prepare a catalog submission\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";
2
+ export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 a shared simulated internet (the world of the current directory)\n volter world init [--install] [--name <world>] [--twins a,b] [--source vendor=package] [--allow-unknown] detect or select vendors, choose installed publishers, 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 env [--template <file>] render deployment .env.world references into dotenv files\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 example list ready application starters included in this CLI release\n volter example <name> [--dir <path>] copy a cookbook starter into an empty directory; does not install or start it\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 generate write seed.generated.ts from world.json's people, organizations and accounts\n volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)\n volter world seed load the default data\n volter world play <scenario.json> reset to the seed, play the scenario's timeline, check its properties\n volter world mark [<id>] a run's position: the twins' logs and the clock (and the app's disk via the fault door)\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>] [--at <mark>|<instant>] list branches, or make one (from here, or from a mark) 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>] [--remote <remote>] [--token <token>] [--force] send entries to the selected remote (default origin); vendor deployment follows its root policy\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 submit --source <id> --vendor <vendor> --package <name> --version <exact> prepare a catalog submission\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
@@ -60,9 +60,11 @@ world — a shared simulated internet (the world of the current directory)
60
60
  volter world generate write seed.generated.ts from world.json's people, organizations and accounts
61
61
  volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)
62
62
  volter world seed load the default data
63
+ volter world play <scenario.json> reset to the seed, play the scenario's timeline, check its properties
64
+ volter world mark [<id>] a run's position: the twins' logs and the clock (and the app's disk via the fault door)
63
65
  volter world reset back to the default data
64
66
  volter world down [--purge] stop the twins (--purge forgets this branch's state)
65
- volter world branch [<name>] list branches, or make one from here and check it out
67
+ volter world branch [<name>] [--at <mark>|<instant>] list branches, or make one (from here, or from a mark) and check it out
66
68
  volter world checkout <name> switch branches
67
69
  volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
68
70
  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
@@ -368,6 +370,32 @@ async function worldCommand(verb, args) {
368
370
  process.exitCode = r.exitCode;
369
371
  return;
370
372
  }
373
+ case 'mark': {
374
+ const m = await world.mark(pos[0]);
375
+ if (asJson) {
376
+ json(m);
377
+ return;
378
+ }
379
+ out(`mark ${m.id} ${m.ledgers.length} twin log${m.ledgers.length === 1 ? '' : 's'} clock ${m.clock ?? 'the machine\'s'} ${m.disk ? 'with the app\'s disk' : 'without the app\'s disk (no fault door)'}`);
380
+ return;
381
+ }
382
+ case 'play': {
383
+ const file = pos[0];
384
+ if (!file)
385
+ throw new Error('volter world play <scenario.json>');
386
+ const r = await world.play(file);
387
+ if (asJson) {
388
+ json(r);
389
+ return;
390
+ }
391
+ out(`played ${r.scenario} ${r.start} → ${r.end} ${r.calls.length} call${r.calls.length === 1 ? '' : 's'}`);
392
+ out(r.completeness.total === 0 ? 'complete the World was the whole internet this run reached' : `incomplete ${Object.entries(r.completeness.egress).map(([d, n]) => `${d} refused ${n.refused}${n.passed ? ` passed ${n.passed}` : ''}`).concat(Object.entries(r.completeness.unimplemented).flatMap(([v, ops]) => Object.entries(ops).map(([op, n]) => `${v} ${op} unimplemented ${n}`))).concat(Object.entries(r.completeness.machine).map(([f, n]) => `${f} fault refused ${n} (no fault door)`)).join('; ')}`);
393
+ for (const p of r.properties)
394
+ out(`${p.verdict.padEnd(8)} ${p.kind.padEnd(10)} ${p.name}${p.step !== undefined ? ` (step ${p.step}, ${p.at})` : ''}`);
395
+ if (r.properties.some((p) => p.verdict === 'broken'))
396
+ process.exitCode = 1;
397
+ return;
398
+ }
371
399
  case 'reset': {
372
400
  const r = await world.reset();
373
401
  out(`Back to the default data on ${r.world}: ${Object.entries(r.observed).map(([s, n]) => `${s} ${n}`).join(', ') || 'nothing recorded'}`);
@@ -383,13 +411,13 @@ async function worldCommand(verb, args) {
383
411
  return;
384
412
  }
385
413
  const atText = value(args, '--at');
386
- const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : (() => {
414
+ const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : !atText.includes('@') ? { mark: atText } : (() => {
387
415
  const positions = {};
388
416
  const views = {};
389
417
  for (const p of atText.split(',')) {
390
418
  const match = /^([^@]+)@(?:([a-f0-9]{64}):)?(\d+)$/.exec(p);
391
419
  if (!match)
392
- throw new Error('--at takes an instant, twin@position, or twin@view:position');
420
+ throw new Error('--at takes an instant, a mark, twin@position, or twin@view:position');
393
421
  positions[match[1]] = Number(match[3]);
394
422
  if (match[2])
395
423
  views[match[1]] = match[2];
@@ -414,23 +442,23 @@ async function worldCommand(verb, args) {
414
442
  if (action === 'set') {
415
443
  if (!arg)
416
444
  throw new Error('volter world clock set <iso-8601>');
417
- out(world.setClock(arg));
445
+ out(await world.setClock(arg));
418
446
  return;
419
447
  }
420
448
  if (action === 'advance') {
421
449
  if (!arg)
422
450
  throw new Error('volter world clock advance <N s|m|h|d>');
423
- out(world.advanceClock(arg));
451
+ out(await world.advanceClock(arg));
424
452
  return;
425
453
  }
426
454
  if (action === 'shift') {
427
455
  if (!arg)
428
456
  throw new Error('volter world clock shift <N s|m|h|d>');
429
- out(world.shiftClock(arg));
457
+ out(await world.shiftClock(arg));
430
458
  return;
431
459
  }
432
460
  if (action === 'clear') {
433
- out(world.clearClock());
461
+ out(await world.clearClock());
434
462
  return;
435
463
  }
436
464
  if (action === undefined || action === 'show') {
@@ -1,3 +1,4 @@
1
1
  export { World, parseOriginUrl, worldNameFor, type LogEntry, type WorldMode, type WorldRef, type WorldStatus } from './world.js';
2
2
  export { findWorldRoot, requireWorldRoot, currentBranch, setCurrentBranch, mainBranch, worldConfigPath, worldEnvPath, worldSeedPath, WORLD_FILE } from './locate.js';
3
3
  export { credentialsPath, requireToken, storeToken, tokenFor } from './credentials.js';
4
+ export { always, sometimes, eventually, type Property, type RunReport } from '@volter/world-runtime';
package/dist/src/index.js CHANGED
@@ -4,3 +4,5 @@
4
4
  export { World, parseOriginUrl, worldNameFor } from "./world.js";
5
5
  export { findWorldRoot, requireWorldRoot, currentBranch, setCurrentBranch, mainBranch, worldConfigPath, worldEnvPath, worldSeedPath, WORLD_FILE } from "./locate.js";
6
6
  export { credentialsPath, requireToken, storeToken, tokenFor } from "./credentials.js";
7
+ // a scenario's properties (ADR 0036 §7): `export default [always(...), sometimes(...), eventually(...)]`
8
+ export { always, sometimes, eventually } from '@volter/world-runtime';
@@ -1,7 +1,9 @@
1
1
  // `volter world browser <name> [url] [--cdp <endpoint>]`: a person's saved World browser opened in a real browser
2
2
  // (ADR 0016, rows 3 and 4). The profile is read through the World's own record reader and the browser is routed through
3
3
  // the World's proxy with its CA trusted, as attach publishes them. Nothing is written back to the World.
4
- import { deriveWorldManifest, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
4
+ import { clockFile, deriveWorldManifest, followWorld, overlayWorldPackFacts, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
5
+ import clockForm from '@volter/world-core/world-clock';
6
+ import { existsSync, readFileSync } from 'node:fs';
5
7
  export async function openWorldBrowser(world, name, url, cdp, say) {
6
8
  const instance = statusWorld(world.name, world.root);
7
9
  const record = worldBrowser(instance.dirs.data, name);
@@ -9,6 +11,7 @@ export async function openWorldBrowser(world, name, url, cdp, say) {
9
11
  const names = worldBrowsers(instance.dirs.data).map(row => row.name);
10
12
  throw new Error(`World "${world.name}" has no browser "${name}" (has: ${names.join(', ') || 'none'})`);
11
13
  }
14
+ overlayWorldPackFacts({ configPath: instance.configPath, root: world.root });
12
15
  const manifest = deriveWorldManifest(world.name, instance.env ?? {});
13
16
  if (!instance.running || !manifest.browser)
14
17
  throw new Error('the World is not running, so it has no proxy to route a browser through: start it with `volter world up`');
@@ -20,10 +23,14 @@ export async function openWorldBrowser(world, name, url, cdp, say) {
20
23
  throw new Error('volter world browser needs playwright: npm i playwright');
21
24
  }
22
25
  const profile = record.state;
26
+ // the page follows the World (ADR 0036 §6): its clock as the World's stands, its randomness seeded by World and browser
27
+ const clock = existsSync(clockFile(world.root, world.name)) ? clockForm.parseClock(readFileSync(clockFile(world.root, world.name), 'utf8')) : null;
28
+ const follow = { now: new Date(clock ? clockForm.clockNowMs(clock, Date.now()) : Date.now()).toISOString(), frozen: clock?.kind === 'frozen', random: `${world.name}:${name}` };
23
29
  if (cdp === undefined) {
24
30
  const browser = await chromium.launch({ headless: false, args: manifest.browser.args });
25
31
  const closed = new Promise(resolve => browser.on('disconnected', () => resolve()));
26
32
  const context = await browser.newContext({ ...profile.context, storageState: profile.storageState });
33
+ await followWorld(context, follow);
27
34
  const page = await context.newPage();
28
35
  await page.goto(url ?? 'about:blank');
29
36
  await closed;
@@ -37,6 +44,7 @@ export async function openWorldBrowser(world, name, url, cdp, say) {
37
44
  if (Object.keys(profile.context).length)
38
45
  say(`note ${name}'s context options (${Object.keys(profile.context).join(', ')}) were not applied: an existing browser's default context takes none`);
39
46
  await context.addCookies(profile.storageState.cookies);
47
+ await followWorld(context, follow);
40
48
  const page = await context.newPage();
41
49
  await page.goto(url ?? 'about:blank');
42
50
  await browser.close();
@@ -1,12 +1,14 @@
1
1
  import { renderDeploymentTemplates, type DeploymentTemplate } from '@volter/world-runtime/deployment-env';
2
2
  import { rebaseChangeset, worldBootMarker, type Changeset, type LedgerDelta, type TwinAction, type WorldMarker, type ApplyReceipt, applyTwinWrite, type TwinResource } from '@volter/world-core/runtime';
3
- import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedServedWorld, seedWorld, statusWorld, writeGeneratedSeed, 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 { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedServedWorld, seedWorld, playWorld, statusWorld, writeGeneratedSeed, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type ViewConsole, type WorldView, rootCredentialInput, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
4
4
  import { rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
5
5
  export type WorldRef = {
6
6
  name?: string;
7
7
  root?: string;
8
8
  };
9
9
  export type WorldMode = 'local' | 'share' | 'sealed';
10
+ /** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
11
+ /** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
10
12
  export declare class TwinLog {
11
13
  readonly service: string;
12
14
  readonly stateService: string;
@@ -153,6 +155,8 @@ export declare class World {
153
155
  hasSeed(): boolean;
154
156
  /** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
155
157
  seedServed(base: string, token: string): ReturnType<typeof seedServedWorld>;
158
+ /** A run of a scenario (ADR 0036): back to the seed, its timeline played at World times, its properties checked. */
159
+ play(scenario: string): ReturnType<typeof playWorld>;
156
160
  /** Back to the default data: forget this branch's state, boot, seed. */
157
161
  reset(opts?: {
158
162
  entry?: string;
@@ -168,6 +172,7 @@ export declare class World {
168
172
  instant?: string;
169
173
  positions?: Record<string, number>;
170
174
  views?: Record<string, string>;
175
+ mark?: string;
171
176
  };
172
177
  }): Promise<World>;
173
178
  /** Switch to a branch: its twins come back with their state; this branch's stop. */
@@ -187,15 +192,17 @@ export declare class World {
187
192
  frozen: boolean;
188
193
  running: boolean;
189
194
  };
190
- /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
191
- setClock(iso: string): string;
192
- /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
193
- advanceClock(by: string): string;
195
+ /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. The
196
+ * clock is written at once; the answer comes once every twin has done what the move made due (ADR 0036 §4). */
197
+ setClock(iso: string): Promise<string>;
198
+ /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. Answers as setClock does. */
199
+ advanceClock(by: string): Promise<string>;
194
200
  /** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
195
- * running clock further. */
196
- shiftClock(by: string): string;
197
- /** Return the World to the machine's time. */
198
- clearClock(): string;
201
+ * running clock further. Answers as setClock does. */
202
+ shiftClock(by: string): Promise<string>;
203
+ /** Return the World to the machine's time. Answers as setClock does: what the move made due is done first. */
204
+ clearClock(): Promise<string>;
205
+ private settled;
199
206
  private writeClock;
200
207
  /** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
201
208
  rebaseBranch(): Array<{
@@ -291,7 +298,9 @@ export declare class World {
291
298
  token?: string;
292
299
  force?: boolean;
293
300
  }): Promise<Array<Awaited<ReturnType<typeof pushWorldChangeset>>>>;
294
- mark(id?: string, now?: Date): WorldMarker;
301
+ /** A mark: every twin's position and history view, and the World clock, a run's position (ADR 0036 §5); the app's disk
302
+ * joins them where the host gives the World its machine's fault door (`VOLTER_WORLD_MACHINE_DOOR`). */
303
+ mark(id?: string, now?: Date): Promise<WorldMarker>;
295
304
  marks(): WorldMarker[];
296
305
  verify(name: string, opts?: {
297
306
  into?: string;
package/dist/src/world.js CHANGED
@@ -7,21 +7,14 @@ import { platformPerson } from "./credentials.js";
7
7
  import { rebaseChangeset, worldBootMarker, listActions, inWorldHistory, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core/runtime';
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import clockForm from '@volter/world-core/world-clock';
10
- import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld, materializeBrowserFiles, statusWorld, writeGeneratedSeed, 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
+ import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld, findWorldMark, machineDoor, materializeBrowserFiles, playWorld, settleLocalWorldTime, statusWorld, writeGeneratedSeed, 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';
11
11
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
12
  import { dirname, join, resolve } from 'node:path';
13
13
  import { requireToken, storeToken } from "./credentials.js";
14
14
  import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigPath, worldEnvPath, worldSeedPath } from "./locate.js";
15
- import { destinationOf, parentEntries, rebaseBranch } from '@volter/world-core/runtime';
15
+ import { destinationOf, durationMs, parentEntries, rebaseBranch } from '@volter/world-core/runtime';
16
16
  /** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
17
17
  /** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
18
- /** `30d`, `12h`, `5m`, `90s` in ms. */
19
- function durationMs(by) {
20
- const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
21
- if (!m)
22
- throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
23
- return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
24
- }
25
18
  export class TwinLog {
26
19
  service;
27
20
  stateService;
@@ -255,6 +248,8 @@ export class World {
255
248
  hasSeed() { return existsSync(worldSeedPath(this.root)); }
256
249
  /** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
257
250
  seedServed(base, token) { return seedServedWorld(base, token, { root: this.root, config: this.configRef(), cwd: this.root }); }
251
+ /** A run of a scenario (ADR 0036): back to the seed, its timeline played at World times, its properties checked. */
252
+ play(scenario) { return playWorld(this.name, { root: this.root, scenario }); }
258
253
  /** Back to the default data: forget this branch's state, boot, seed. */
259
254
  reset(opts = {}) { return resetWorld(this.name, { root: this.root, cwd: this.root, ...opts }); }
260
255
  // ── branches ────────────────────────────────────────────────────────────────────────────
@@ -264,6 +259,14 @@ export class World {
264
259
  * one starts; `checkout` brings it back with its state.
265
260
  */
266
261
  async branch(name, opts = {}) {
262
+ // at a mark: from the branch it was taken on, at each twin's view and position there, with its clock (and the app's
263
+ // disk through the machine's fault door, where the mark took it)
264
+ const mark = opts.at?.mark !== undefined ? findWorldMark(opts.at.mark, this.root) : undefined;
265
+ const door = mark?.disk ? machineDoor() : null;
266
+ if (mark?.disk && !door)
267
+ throw new Error(`mark "${mark.id}" holds the app's disk, and this host gives no fault door to restore it through (VOLTER_WORLD_MACHINE_DOOR)`);
268
+ const base = mark?.world ?? this.name;
269
+ const at = mark ? { positions: Object.fromEntries(mark.ledgers.flatMap((l) => (l.position !== undefined ? [[l.stateService, l.position]] : []))), views: Object.fromEntries(mark.ledgers.flatMap((l) => (l.view !== undefined ? [[l.stateService, l.view]] : []))) } : opts.at;
267
270
  if (listWorlds(this.root).some((w) => w.name === name))
268
271
  throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
269
272
  this.refuseWhileServed('branch');
@@ -273,7 +276,7 @@ export class World {
273
276
  if (wasRunning)
274
277
  await downWorld(this.name, this.root);
275
278
  try {
276
- await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
279
+ await branchWorld(base, name, { root: this.root, envFile: worldEnvPath(this.root), ...(at ? { at } : {}) });
277
280
  }
278
281
  catch (error) {
279
282
  if (wasRunning)
@@ -283,7 +286,16 @@ export class World {
283
286
  setCurrentBranch(this.root, name);
284
287
  const selected = statusWorld(name, this.root);
285
288
  materializeBrowserFiles(selected.dirs.data, this.root, name, selected.config);
286
- return new World(name, this.root);
289
+ const made = new World(name, this.root);
290
+ if (mark) {
291
+ if (mark.clock)
292
+ await made.setClock(mark.clock);
293
+ else if (mark.clock === null)
294
+ await made.clearClock();
295
+ if (door)
296
+ await door.restorePosition(mark.id);
297
+ }
298
+ return made;
287
299
  }
288
300
  /** Switch to a branch: its twins come back with their state; this branch's stop. */
289
301
  async checkout(name) {
@@ -333,25 +345,26 @@ export class World {
333
345
  const c = clockForm.parseClock(readFileSync(file, 'utf8'));
334
346
  return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
335
347
  }
336
- /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
348
+ /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. The
349
+ * clock is written at once; the answer comes once every twin has done what the move made due (ADR 0036 §4). */
337
350
  setClock(iso) {
338
351
  const parsed = Date.parse(iso);
339
352
  if (Number.isNaN(parsed))
340
353
  throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
341
354
  this.writeClock({ kind: 'frozen', at: parsed });
342
- return new Date(parsed).toISOString();
355
+ return this.settled(new Date(parsed).toISOString());
343
356
  }
344
- /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
357
+ /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. Answers as setClock does. */
345
358
  advanceClock(by) {
346
359
  const file = clockFile(this.root, this.name);
347
360
  if (!existsSync(file))
348
361
  throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
349
362
  const c = clockForm.parseClock(readFileSync(file, 'utf8'));
350
363
  this.writeClock({ ...c, at: c.at + durationMs(by) });
351
- return this.clock().at;
364
+ return this.settled(this.clock().at);
352
365
  }
353
366
  /** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
354
- * running clock further. */
367
+ * running clock further. Answers as setClock does. */
355
368
  shiftClock(by) {
356
369
  const file = clockFile(this.root, this.name);
357
370
  const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
@@ -359,14 +372,18 @@ export class World {
359
372
  throw new Error('the clock is frozen — `volter world clock advance` moves it');
360
373
  const wall = Date.now();
361
374
  this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
362
- return this.clock().at;
375
+ return this.settled(this.clock().at);
363
376
  }
364
- /** Return the World to the machine's time. */
377
+ /** Return the World to the machine's time. Answers as setClock does: what the move made due is done first. */
365
378
  clearClock() {
366
379
  const file = clockFile(this.root, this.name);
367
380
  if (existsSync(file))
368
381
  rmSync(file);
369
- return new Date().toISOString();
382
+ return this.settled(new Date().toISOString());
383
+ }
384
+ async settled(at) {
385
+ await settleLocalWorldTime(this.name, this.root);
386
+ return at;
370
387
  }
371
388
  writeClock(c) {
372
389
  const file = clockFile(this.root, this.name);
@@ -562,7 +579,15 @@ export class World {
562
579
  return outcomes;
563
580
  }
564
581
  // ── review (the operator's verbs, kept on the SDK for the remote and for CI) ────────────
565
- mark(id, now) { return markWorld(this.name, { root: this.root, ...(id ? { id } : {}), ...(now ? { now } : {}) }); }
582
+ /** A mark: every twin's position and history view, and the World clock, a run's position (ADR 0036 §5); the app's disk
583
+ * joins them where the host gives the World its machine's fault door (`VOLTER_WORLD_MACHINE_DOOR`). */
584
+ async mark(id, now) {
585
+ const door = machineDoor();
586
+ const marker = markWorld(this.name, { root: this.root, ...(id ? { id } : {}), ...(now ? { now } : {}), ...(door ? { disk: true } : {}) });
587
+ if (door)
588
+ await door.takePosition(marker.id);
589
+ return marker;
590
+ }
566
591
  marks() { return listWorldMarks(this.name, this.root); }
567
592
  verify(name, opts = { ephemeral: true }) { return verifyWorldChangeset(name, { root: this.root, world: this.name, ...opts }); }
568
593
  approve(name, principal, note) { return approveWorldChangeset(name, { root: this.root, world: this.name, principal, ...(note ? { note } : {}) }); }
@@ -13,7 +13,7 @@ You need Node 22.6+, pnpm 12.15.0, Docker Compose and registry access during ins
13
13
  Copy this starter into a new folder:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.193 example customer-onboarding --dir onboarding
16
+ $ npx @volter/world@3.0.194 example customer-onboarding --dir onboarding
17
17
  $ cd onboarding
18
18
  $ pnpm install
19
19
  ```
@@ -7,22 +7,22 @@ try {
7
7
  await initialize();
8
8
  switch (process.argv[2]) {
9
9
  case 'baseline': {
10
- world.setClock(at);
10
+ await world.setClock(at);
11
11
  const rows = await listSignups();
12
12
  if (rows.length > 1 || rows.some(r => r.name !== 'Grace')) throw new Error('This demo contains customer work; reset both your disposable application database and demo World explicitly.');
13
13
  const grace = rows.find(r => r.name === 'Grace') ?? await onboard({ name: 'Grace', email: 'owner@world.test' });
14
- world.advanceClock('5s');
14
+ await world.advanceClock('5s');
15
15
  console.log('Baseline', JSON.stringify(summarize((await listSignups()).find(r => r.customer_id === grace.customer_id)))); break;
16
16
  }
17
17
  case 'present': {
18
18
  const rows = await listSignups();
19
19
  if (rows.length !== 1 || rows[0].name !== 'Grace') throw new Error('Prepare the Grace-only baseline first.');
20
- world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
20
+ await world.setClock(new Date(Date.parse(at) + 10_000).toISOString());
21
21
  const partial = await onboard({ name: 'Ada', email: 'owner@world.test', from: 'Example <welcome@unverified.world.test>' });
22
22
  console.log('Email refusal; customer and SQL row retained', JSON.stringify(summarize(partial)));
23
23
  const retry = await retryWelcome(partial.customer_id);
24
24
  console.log('Retry accepted on the same customer', JSON.stringify(summarize(retry)));
25
- world.advanceClock('5s');
25
+ await world.advanceClock('5s');
26
26
  console.log('Delivered', JSON.stringify((await listSignups()).map(summarize))); break;
27
27
  }
28
28
  default: throw new Error('Use baseline or present');
@@ -11,13 +11,13 @@
11
11
  "pg": "8.16.3"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.193",
15
- "@volter/world-core": "3.0.166",
16
- "@volter/world-runtime": "3.0.191",
17
- "@volter/world-console": "3.0.161",
14
+ "@volter/world": "3.0.194",
15
+ "@volter/world-core": "3.0.167",
16
+ "@volter/world-runtime": "3.0.192",
17
+ "@volter/world-console": "3.0.162",
18
18
  "@volter/twin-stripe": "3.0.2",
19
19
  "@volter/twin-resend": "1.0.1",
20
- "@volter/world-ui": "1.0.165"
20
+ "@volter/world-ui": "1.0.166"
21
21
  },
22
22
  "packageManager": "pnpm@12.15.0",
23
23
  "devEngines": {
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
13
13
  Use an empty parent folder. Create the baseline starter there:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.193 example integration-branches --dir baseline
16
+ $ npx @volter/world@3.0.194 example integration-branches --dir baseline
17
17
  $ cd baseline
18
18
  $ pnpm install
19
19
  $ ./node_modules/.bin/volter world init --name baseline --twins stripe
@@ -38,9 +38,9 @@ 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.193 example integration-branches --dir integration
42
- $ npx @volter/world@3.0.193 example integration-branches --dir worker-a
43
- $ npx @volter/world@3.0.193 example integration-branches --dir worker-b
41
+ $ npx @volter/world@3.0.194 example integration-branches --dir integration
42
+ $ npx @volter/world@3.0.194 example integration-branches --dir worker-a
43
+ $ npx @volter/world@3.0.194 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:
@@ -9,12 +9,12 @@
9
9
  "stripe": "17.7.0"
10
10
  },
11
11
  "devDependencies": {
12
- "@volter/world": "3.0.193",
13
- "@volter/world-core": "3.0.166",
14
- "@volter/world-runtime": "3.0.191",
15
- "@volter/world-console": "3.0.161",
12
+ "@volter/world": "3.0.194",
13
+ "@volter/world-core": "3.0.167",
14
+ "@volter/world-runtime": "3.0.192",
15
+ "@volter/world-console": "3.0.162",
16
16
  "@volter/twin-stripe": "3.0.2",
17
- "@volter/world-ui": "1.0.165"
17
+ "@volter/world-ui": "1.0.166"
18
18
  },
19
19
  "packageManager": "pnpm@12.15.0",
20
20
  "devEngines": {
@@ -19,7 +19,7 @@ The catalog offers current defaults. Keep this example's manifest and installati
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.193 example workplace-agent --dir workplace
22
+ $ npx @volter/world@3.0.194 example workplace-agent --dir workplace
23
23
  $ cd workplace
24
24
  $ pnpm install
25
25
  $ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
@@ -11,14 +11,14 @@
11
11
  "@slack/web-api": "7.19.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.193",
15
- "@volter/world-core": "3.0.166",
16
- "@volter/world-runtime": "3.0.191",
17
- "@volter/world-console": "3.0.161",
14
+ "@volter/world": "3.0.194",
15
+ "@volter/world-core": "3.0.167",
16
+ "@volter/world-runtime": "3.0.192",
17
+ "@volter/world-console": "3.0.162",
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.165"
21
+ "@volter/world-ui": "1.0.166"
22
22
  },
23
23
  "packageManager": "pnpm@12.15.0",
24
24
  "devEngines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world",
3
- "version": "3.0.193",
3
+ "version": "3.0.194",
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.166",
35
- "@volter/world-runtime": "3.0.191",
34
+ "@volter/world-core": "3.0.167",
35
+ "@volter/world-runtime": "3.0.192",
36
36
  "zod": "^4.6.5"
37
37
  },
38
38
  "peerDependencies": {
39
- "@volter/world-console": "3.0.161",
39
+ "@volter/world-console": "3.0.162",
40
40
  "playwright": "^1.63.0"
41
41
  },
42
42
  "peerDependenciesMeta": {
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@volter/twin-anthropic": "1.0.6",
52
- "@volter/world-console": "3.0.161"
52
+ "@volter/world-console": "3.0.162"
53
53
  },
54
54
  "bin": {
55
55
  "volter": "dist/src/cli.js"
@@ -117,10 +117,12 @@ Identity comes from credentials in the vendor's manner; inspect `GET /twin` for
117
117
  identity and time rules. Use `clock set` and `clock advance` for backdated history. Without a
118
118
  frozen World clock, the runtime uses wall time.
119
119
 
120
- Author judgment, stateless lookups and faults in `handlers/<vendor>.json` in the World config
121
- directory (typically `.volter/handlers/`). These are ordered, first-match scenario rules.
122
- Edit the file and restart to load changes; a handler must never fake success for a stored
123
- mutation. `GET /twin/scenario` shows handler matches and misses; `tail` records misses to author.
120
+ Place a vendor's judgment (a decision) and failures (faults) on a scenario's timeline,
121
+ `scenarios/<name>.json` beside `world.json` (typically `.volter/scenarios/`), and run it with
122
+ `volter world play <file>`: it resets to the default data, sets the clock to the scenario's
123
+ `start` and makes the scenario the World's, which every twin reads at each request. A twin turns
124
+ each input into its own answer and records it; nothing in a scenario answers in its place.
125
+ `GET /twin/scenario` shows what is placed on that twin and what it consumed.
124
126
  Model twins serve deterministic scripts or labeled stubs, so verify calls and state changes,
125
127
  not the quality of prose from an unconfigured model.
126
128
 
package/src/cli.ts CHANGED
@@ -51,9 +51,11 @@ world — a shared simulated internet (the world of the current directory)
51
51
  volter world generate write seed.generated.ts from world.json's people, organizations and accounts
52
52
  volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)
53
53
  volter world seed load the default data
54
+ volter world play <scenario.json> reset to the seed, play the scenario's timeline, check its properties
55
+ volter world mark [<id>] a run's position: the twins' logs and the clock (and the app's disk via the fault door)
54
56
  volter world reset back to the default data
55
57
  volter world down [--purge] stop the twins (--purge forgets this branch's state)
56
- volter world branch [<name>] list branches, or make one from here and check it out
58
+ volter world branch [<name>] [--at <mark>|<instant>] list branches, or make one (from here, or from a mark) and check it out
57
59
  volter world checkout <name> switch branches
58
60
  volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
59
61
  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
@@ -302,6 +304,23 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
302
304
  if (r.exitCode !== 0) process.exitCode = r.exitCode;
303
305
  return;
304
306
  }
307
+ case 'mark': {
308
+ const m = await world.mark(pos[0]);
309
+ if (asJson) { json(m); return; }
310
+ out(`mark ${m.id} ${m.ledgers.length} twin log${m.ledgers.length === 1 ? '' : 's'} clock ${m.clock ?? 'the machine\'s'} ${m.disk ? 'with the app\'s disk' : 'without the app\'s disk (no fault door)'}`);
311
+ return;
312
+ }
313
+ case 'play': {
314
+ const file = pos[0];
315
+ if (!file) throw new Error('volter world play <scenario.json>');
316
+ const r = await world.play(file);
317
+ if (asJson) { json(r); return; }
318
+ out(`played ${r.scenario} ${r.start} → ${r.end} ${r.calls.length} call${r.calls.length === 1 ? '' : 's'}`);
319
+ out(r.completeness.total === 0 ? 'complete the World was the whole internet this run reached' : `incomplete ${Object.entries(r.completeness.egress).map(([d, n]) => `${d} refused ${n.refused}${n.passed ? ` passed ${n.passed}` : ''}`).concat(Object.entries(r.completeness.unimplemented).flatMap(([v, ops]) => Object.entries(ops).map(([op, n]) => `${v} ${op} unimplemented ${n}`))).concat(Object.entries(r.completeness.machine).map(([f, n]) => `${f} fault refused ${n} (no fault door)`)).join('; ')}`);
320
+ for (const p of r.properties) out(`${p.verdict.padEnd(8)} ${p.kind.padEnd(10)} ${p.name}${p.step !== undefined ? ` (step ${p.step}, ${p.at})` : ''}`);
321
+ if (r.properties.some((p) => p.verdict === 'broken')) process.exitCode = 1;
322
+ return;
323
+ }
305
324
  case 'reset': {
306
325
  const r = await world.reset();
307
326
  out(`Back to the default data on ${r.world}: ${Object.entries(r.observed).map(([s, n]) => `${s} ${n}`).join(', ') || 'nothing recorded'}`);
@@ -312,11 +331,11 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
312
331
  const name = pos[0];
313
332
  if (!name) { for (const b of world.branches()) out(`${b === world.name ? '* ' : ' '}${b}`); return; }
314
333
  const atText = value(args, '--at');
315
- const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : (() => {
334
+ const at = atText === undefined ? undefined : /^\d{4}-\d\d-\d\d/.test(atText) ? { instant: atText } : !atText.includes('@') ? { mark: atText } : (() => {
316
335
  const positions: Record<string, number> = {}; const views: Record<string, string> = {};
317
336
  for (const p of atText.split(',')) {
318
337
  const match = /^([^@]+)@(?:([a-f0-9]{64}):)?(\d+)$/.exec(p);
319
- if (!match) throw new Error('--at takes an instant, twin@position, or twin@view:position');
338
+ if (!match) throw new Error('--at takes an instant, a mark, twin@position, or twin@view:position');
320
339
  positions[match[1]!] = Number(match[3]); if (match[2]) views[match[1]!] = match[2];
321
340
  }
322
341
  return { positions, views };
@@ -335,10 +354,10 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
335
354
  }
336
355
  case 'clock': {
337
356
  const [action, arg] = pos;
338
- if (action === 'set') { if (!arg) throw new Error('volter world clock set <iso-8601>'); out(world.setClock(arg)); return; }
339
- if (action === 'advance') { if (!arg) throw new Error('volter world clock advance <N s|m|h|d>'); out(world.advanceClock(arg)); return; }
340
- if (action === 'shift') { if (!arg) throw new Error('volter world clock shift <N s|m|h|d>'); out(world.shiftClock(arg)); return; }
341
- if (action === 'clear') { out(world.clearClock()); return; }
357
+ if (action === 'set') { if (!arg) throw new Error('volter world clock set <iso-8601>'); out(await world.setClock(arg)); return; }
358
+ if (action === 'advance') { if (!arg) throw new Error('volter world clock advance <N s|m|h|d>'); out(await world.advanceClock(arg)); return; }
359
+ if (action === 'shift') { if (!arg) throw new Error('volter world clock shift <N s|m|h|d>'); out(await world.shiftClock(arg)); return; }
360
+ if (action === 'clear') { out(await world.clearClock()); return; }
342
361
  if (action === undefined || action === 'show') { const c = world.clock(); out(`${c.at}${c.frozen ? '' : c.running ? ' (running)' : ' (wall clock — not set)'}`); return; }
343
362
  throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear');
344
363
  }
package/src/index.ts CHANGED
@@ -4,3 +4,5 @@
4
4
  export { World, parseOriginUrl, worldNameFor, type LogEntry, type WorldMode, type WorldRef, type WorldStatus } from './world.ts';
5
5
  export { findWorldRoot, requireWorldRoot, currentBranch, setCurrentBranch, mainBranch, worldConfigPath, worldEnvPath, worldSeedPath, WORLD_FILE } from './locate.ts';
6
6
  export { credentialsPath, requireToken, storeToken, tokenFor } from './credentials.ts';
7
+ // a scenario's properties (ADR 0036 §7): `export default [always(...), sometimes(...), eventually(...)]`
8
+ export { always, sometimes, eventually, type Property, type RunReport } from '@volter/world-runtime';
package/src/sdk.test.ts CHANGED
@@ -68,8 +68,8 @@ describe('World — the twins an app needs, running together', () => {
68
68
  await expect(back.checkout('nonesuch')).rejects.toThrow(/No branch "nonesuch"/);
69
69
 
70
70
  // down stops compute; up resumes the branch WITH its state (reset is the way back to the defaults)
71
- back.setClock(AT);
72
- back.advanceClock('1h');
71
+ await back.setClock(AT);
72
+ await back.advanceClock('1h');
73
73
  const retainedClock = back.clock();
74
74
  await back.down();
75
75
  expect(back.status().running).toBe(false);
@@ -1,7 +1,9 @@
1
1
  // `volter world browser <name> [url] [--cdp <endpoint>]`: a person's saved World browser opened in a real browser
2
2
  // (ADR 0016, rows 3 and 4). The profile is read through the World's own record reader and the browser is routed through
3
3
  // the World's proxy with its CA trusted, as attach publishes them. Nothing is written back to the World.
4
- import { deriveWorldManifest, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
4
+ import { clockFile, deriveWorldManifest, followWorld, overlayWorldPackFacts, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
5
+ import clockForm from '@volter/world-core/world-clock';
6
+ import { existsSync, readFileSync } from 'node:fs';
5
7
 
6
8
  export async function openWorldBrowser(world: { name: string; root: string }, name: string, url: string | undefined, cdp: string | undefined, say: (line: string) => void): Promise<void> {
7
9
  const instance = statusWorld(world.name, world.root);
@@ -10,16 +12,21 @@ export async function openWorldBrowser(world: { name: string; root: string }, na
10
12
  const names = worldBrowsers(instance.dirs.data).map(row => row.name);
11
13
  throw new Error(`World "${world.name}" has no browser "${name}" (has: ${names.join(', ') || 'none'})`);
12
14
  }
15
+ overlayWorldPackFacts({ configPath: instance.configPath, root: world.root });
13
16
  const manifest = deriveWorldManifest(world.name, instance.env ?? {});
14
17
  if (!instance.running || !manifest.browser) throw new Error('the World is not running, so it has no proxy to route a browser through: start it with `volter world up`');
15
18
  let chromium: typeof import('playwright').chromium;
16
19
  try { ({ chromium } = await import('playwright')); }
17
20
  catch { throw new Error('volter world browser needs playwright: npm i playwright'); }
18
21
  const profile = record.state;
22
+ // the page follows the World (ADR 0036 §6): its clock as the World's stands, its randomness seeded by World and browser
23
+ const clock = existsSync(clockFile(world.root, world.name)) ? clockForm.parseClock(readFileSync(clockFile(world.root, world.name), 'utf8')) : null;
24
+ const follow = { now: new Date(clock ? clockForm.clockNowMs(clock, Date.now()) : Date.now()).toISOString(), frozen: clock?.kind === 'frozen', random: `${world.name}:${name}` };
19
25
  if (cdp === undefined) {
20
26
  const browser = await chromium.launch({ headless: false, args: manifest.browser.args });
21
27
  const closed = new Promise<void>(resolve => browser.on('disconnected', () => resolve()));
22
28
  const context = await browser.newContext({ ...profile.context, storageState: profile.storageState } as Parameters<typeof browser.newContext>[0]);
29
+ await followWorld(context, follow);
23
30
  const page = await context.newPage();
24
31
  await page.goto(url ?? 'about:blank');
25
32
  await closed;
@@ -31,6 +38,7 @@ export async function openWorldBrowser(world: { name: string; root: string }, na
31
38
  // An existing default context takes no context options (ADR 0016): its cookies are what is loaded.
32
39
  if (Object.keys(profile.context).length) say(`note ${name}'s context options (${Object.keys(profile.context).join(', ')}) were not applied: an existing browser's default context takes none`);
33
40
  await context.addCookies(profile.storageState.cookies as Parameters<typeof context.addCookies>[0]);
41
+ await followWorld(context, follow);
34
42
  const page = await context.newPage();
35
43
  await page.goto(url ?? 'about:blank');
36
44
  await browser.close();
package/src/world.ts CHANGED
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
9
9
  import clockForm from '@volter/world-core/world-clock';
10
10
  import {
11
11
  activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld,
12
- materializeBrowserFiles, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
12
+ findWorldMark, machineDoor, materializeBrowserFiles, playWorld, settleLocalWorldTime, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
13
13
  bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, type ServedWorld, type ViewConsole, type WorldView, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn,
14
14
  type DeployTwinOutcome, type MaterializedRoot,
15
15
  type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
@@ -17,19 +17,13 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { requireToken, storeToken } from './credentials.ts';
19
19
  import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigPath, worldEnvPath, worldSeedPath } from './locate.ts';
20
- import { destinationOf, parentEntries, rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
20
+ import { destinationOf, durationMs, parentEntries, rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
21
21
 
22
22
  export type WorldRef = { name?: string; root?: string };
23
23
  export type WorldMode = 'local' | 'share' | 'sealed';
24
24
 
25
25
  /** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
26
26
  /** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
27
- /** `30d`, `12h`, `5m`, `90s` in ms. */
28
- function durationMs(by: string): number {
29
- const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
30
- if (!m) throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
31
- return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as 's' | 'm' | 'h' | 'd'];
32
- }
33
27
 
34
28
  export class TwinLog {
35
29
  constructor(readonly service: string, readonly stateService: string, readonly root: string) {}
@@ -248,6 +242,8 @@ export class World {
248
242
  hasSeed(): boolean { return existsSync(worldSeedPath(this.root)); }
249
243
  /** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
250
244
  seedServed(base: string, token: string): ReturnType<typeof seedServedWorld> { return seedServedWorld(base, token, { root: this.root, config: this.configRef(), cwd: this.root }); }
245
+ /** A run of a scenario (ADR 0036): back to the seed, its timeline played at World times, its properties checked. */
246
+ play(scenario: string): ReturnType<typeof playWorld> { return playWorld(this.name, { root: this.root, scenario }); }
251
247
  /** Back to the default data: forget this branch's state, boot, seed. */
252
248
  reset(opts: { entry?: string; cwd?: string } = {}): ReturnType<typeof resetWorld> { return resetWorld(this.name, { root: this.root, cwd: this.root, ...opts }); }
253
249
 
@@ -257,7 +253,14 @@ export class World {
257
253
  * at a time in a world (the app's env names one set of twins), so this branch stops as the new
258
254
  * one starts; `checkout` brings it back with its state.
259
255
  */
260
- async branch(name: string, opts: { at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string> } } = {}): Promise<World> {
256
+ async branch(name: string, opts: { at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string>; mark?: string } } = {}): Promise<World> {
257
+ // at a mark: from the branch it was taken on, at each twin's view and position there, with its clock (and the app's
258
+ // disk through the machine's fault door, where the mark took it)
259
+ const mark = opts.at?.mark !== undefined ? findWorldMark(opts.at.mark, this.root) : undefined;
260
+ const door = mark?.disk ? machineDoor() : null;
261
+ if (mark?.disk && !door) throw new Error(`mark "${mark.id}" holds the app's disk, and this host gives no fault door to restore it through (VOLTER_WORLD_MACHINE_DOOR)`);
262
+ const base = mark?.world ?? this.name;
263
+ const at = mark ? { positions: Object.fromEntries(mark.ledgers.flatMap((l) => (l.position !== undefined ? [[l.stateService, l.position]] : []))), views: Object.fromEntries(mark.ledgers.flatMap((l) => (l.view !== undefined ? [[l.stateService, l.view]] : []))) } : opts.at;
261
264
  if (listWorlds(this.root).some((w) => w.name === name)) throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
262
265
  this.refuseWhileServed('branch');
263
266
  // this branch stops first (a managed database is copied from a stopped World, and the two would share its port),
@@ -265,7 +268,7 @@ export class World {
265
268
  const wasRunning = !!this.instance()?.running;
266
269
  if (wasRunning) await downWorld(this.name, this.root);
267
270
  try {
268
- await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
271
+ await branchWorld(base, name, { root: this.root, envFile: worldEnvPath(this.root), ...(at ? { at } : {}) });
269
272
  } catch (error) {
270
273
  if (wasRunning) await checkoutWorld(this.name, { root: this.root });
271
274
  throw error;
@@ -273,7 +276,12 @@ export class World {
273
276
  setCurrentBranch(this.root, name);
274
277
  const selected = statusWorld(name, this.root);
275
278
  materializeBrowserFiles(selected.dirs.data, this.root, name, selected.config);
276
- return new World(name, this.root);
279
+ const made = new World(name, this.root);
280
+ if (mark) {
281
+ if (mark.clock) await made.setClock(mark.clock); else if (mark.clock === null) await made.clearClock();
282
+ if (door) await door.restorePosition(mark.id);
283
+ }
284
+ return made;
277
285
  }
278
286
  /** Switch to a branch: its twins come back with their state; this branch's stop. */
279
287
  async checkout(name: string): Promise<World> {
@@ -315,36 +323,41 @@ export class World {
315
323
  const c = clockForm.parseClock(readFileSync(file, 'utf8'));
316
324
  return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
317
325
  }
318
- /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
319
- setClock(iso: string): string {
326
+ /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. The
327
+ * clock is written at once; the answer comes once every twin has done what the move made due (ADR 0036 §4). */
328
+ setClock(iso: string): Promise<string> {
320
329
  const parsed = Date.parse(iso);
321
330
  if (Number.isNaN(parsed)) throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
322
331
  this.writeClock({ kind: 'frozen', at: parsed });
323
- return new Date(parsed).toISOString();
332
+ return this.settled(new Date(parsed).toISOString());
324
333
  }
325
- /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
326
- advanceClock(by: string): string {
334
+ /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. Answers as setClock does. */
335
+ advanceClock(by: string): Promise<string> {
327
336
  const file = clockFile(this.root, this.name);
328
337
  if (!existsSync(file)) throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
329
338
  const c = clockForm.parseClock(readFileSync(file, 'utf8'));
330
339
  this.writeClock({ ...c, at: c.at + durationMs(by) });
331
- return this.clock().at;
340
+ return this.settled(this.clock().at);
332
341
  }
333
342
  /** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
334
- * running clock further. */
335
- shiftClock(by: string): string {
343
+ * running clock further. Answers as setClock does. */
344
+ shiftClock(by: string): Promise<string> {
336
345
  const file = clockFile(this.root, this.name);
337
346
  const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
338
347
  if (c?.kind === 'frozen') throw new Error('the clock is frozen — `volter world clock advance` moves it');
339
348
  const wall = Date.now();
340
349
  this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
341
- return this.clock().at;
350
+ return this.settled(this.clock().at);
342
351
  }
343
- /** Return the World to the machine's time. */
344
- clearClock(): string {
352
+ /** Return the World to the machine's time. Answers as setClock does: what the move made due is done first. */
353
+ clearClock(): Promise<string> {
345
354
  const file = clockFile(this.root, this.name);
346
355
  if (existsSync(file)) rmSync(file);
347
- return new Date().toISOString();
356
+ return this.settled(new Date().toISOString());
357
+ }
358
+ private async settled(at: string): Promise<string> {
359
+ await settleLocalWorldTime(this.name, this.root);
360
+ return at;
348
361
  }
349
362
  private writeClock(c: Parameters<typeof clockForm.formatClock>[0]): void {
350
363
  const file = clockFile(this.root, this.name);
@@ -509,7 +522,14 @@ export class World {
509
522
  }
510
523
 
511
524
  // ── review (the operator's verbs, kept on the SDK for the remote and for CI) ────────────
512
- mark(id?: string, now?: Date): WorldMarker { return markWorld(this.name, { root: this.root, ...(id ? { id } : {}), ...(now ? { now } : {}) }); }
525
+ /** A mark: every twin's position and history view, and the World clock, a run's position (ADR 0036 §5); the app's disk
526
+ * joins them where the host gives the World its machine's fault door (`VOLTER_WORLD_MACHINE_DOOR`). */
527
+ async mark(id?: string, now?: Date): Promise<WorldMarker> {
528
+ const door = machineDoor();
529
+ const marker = markWorld(this.name, { root: this.root, ...(id ? { id } : {}), ...(now ? { now } : {}), ...(door ? { disk: true } : {}) });
530
+ if (door) await door.takePosition(marker.id);
531
+ return marker;
532
+ }
513
533
  marks(): WorldMarker[] { return listWorldMarks(this.name, this.root); }
514
534
  verify(name: string, opts: { into?: string; ephemeral?: boolean } = { ephemeral: true }): ReturnType<typeof verifyWorldChangeset> { return verifyWorldChangeset(name, { root: this.root, world: this.name, ...opts }); }
515
535
  approve(name: string, principal: string, note?: string): ReturnType<typeof approveWorldChangeset> { return approveWorldChangeset(name, { root: this.root, world: this.name, principal, ...(note ? { note } : {}) }); }
@@ -1,51 +0,0 @@
1
- // A world in the app repo carries its handlers as ROOT-RELATIVE paths (docs/concepts/worlds.md
2
- // #the-config-and-the-running-world): init copies a twin's starter handlers to .volter/handlers/<vendor>.json and
3
- // names that path relative to the world root, so the committed config reads the same on every
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, its files linked from the pack repository: its starter
6
- // handlers are the scripted turns of its own life (journeys/scenario.json).
7
- import { afterAll, describe, expect, test } from 'bun:test';
8
- import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
9
- import { tmpdir } from 'node:os';
10
- import { join } from 'node:path';
11
- import { installedPackageDir } from '@volter/world-core/runtime-state';
12
- import { World } from './index.ts';
13
-
14
- function app(): string {
15
- const parent = mkdtempSync(join(tmpdir(), 'handlers-in-repo-'));
16
- const dir = join(parent, 'agent');
17
- mkdirSync(dir);
18
- writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'agent', dependencies: { '@anthropic-ai/sdk': '^0.30' } }));
19
- writeFileSync(join(dir, '.env.example'), 'ANTHROPIC_API_KEY=\n');
20
- const installed = join(parent, 'node_modules', '@volter', 'twin-anthropic');
21
- mkdirSync(installed, { recursive: true });
22
- // the published pack, this package's dev dependency (ADR 0034, decision 6)
23
- const pack = installedPackageDir(import.meta.dir, '@volter/twin-anthropic')!;
24
- for (const entry of readdirSync(pack)) {
25
- if (entry === 'package.json') copyFileSync(join(pack, entry), join(installed, entry));
26
- else symlinkSync(join(pack, entry), join(installed, entry));
27
- }
28
- return dir;
29
- }
30
-
31
- const root = app();
32
- const world = World.init(root).world;
33
- afterAll(async () => { await world.down().catch(() => undefined); });
34
-
35
- describe('handlers in the app repo', () => {
36
- test('init copies the starter handlers and names them relative to the root', () => {
37
- expect(readFileSync(join(root, '.volter', 'handlers', 'anthropic.json'), 'utf8')).toContain('"handlers"');
38
- const config = JSON.parse(readFileSync(join(root, '.volter', 'world.json'), 'utf8')) as { schemaVersion: number; services: Array<{ id: string; execution?: { process?: { args?: string[] }; colocate?: { scenarioPath?: string } } }> };
39
- expect(config.schemaVersion).toBe(2);
40
- const anthropic = config.services.find((s) => s.id === 'anthropic')!;
41
- expect(anthropic.execution?.process?.args).toEqual(['--scenario', '.volter/handlers/anthropic.json']);
42
- expect(anthropic.execution?.colocate?.scenarioPath).toBe('.volter/handlers/anthropic.json');
43
- });
44
-
45
- test('up resolves them against the root, and the twin serves them', async () => {
46
- const instance = await world.up({ seed: false });
47
- const url = instance.services.anthropic!.url!;
48
- const scenario = await fetch(`${url}/twin/scenario`).then((r) => r.json()) as { handlers?: Array<{ id: string }> };
49
- expect((scenario.handlers ?? []).map((h) => h.id)).toContain('draft-result');
50
- });
51
- });