@pouchy_ai/world-sdk 0.19.0 → 0.22.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.22.0
4
+
5
+ - **`startNextEpisode(environmentId, worldInstanceId)`** — start the next
6
+ episode of a worldline that just finished one, and receive the CARRYOVER.
7
+ - The worldline does **not** restart. Its `worldInstanceId`, membership, state
8
+ and ledger all continue; the episode moves on. Nothing is copied and nothing
9
+ is deleted, so there is no migration to get wrong.
10
+ - **Idempotent.** The run id is derived from the worldline, the episode and the
11
+ revision it starts at, so a retry lands on the same run and answers
12
+ `created: false` rather than forking the story.
13
+ - **Owner token only** — no `/admin` mirror, same reasoning as `preflightWorld`:
14
+ the mirror carries reads, and this is the write that decides a story moves on.
15
+ - New types: `WorldCarryoverV1`, `WorldNextEpisodeResponse`. The carryover is a
16
+ **projection** of facts the worldline already holds — canon it established,
17
+ where the characters stand, threads left hanging, setups never paid off.
18
+ Role-private notes are never in it, and no memory is carried because the world
19
+ runtime writes none.
20
+ - Server-side this is world API 1.28.0.
21
+
22
+ ## 0.21.0
23
+
24
+ - **`WorldProgressCheckpointV1.episode`** — which episode a serial worldline is
25
+ playing, its beats-spent and turn budget, and the ending once it has one.
26
+ Absent for every world whose pinned story is not a serial, which is most of
27
+ them, so nothing an existing integration reads has changed shape.
28
+ - **Type-only.** No new method, no new route: `getProgress` already returned
29
+ this checkpoint and now returns one more optional field.
30
+ - `beatsCommitted` and `turnBudget` are two numbers, **not a ratio** — the same
31
+ rule as `completedNodeCount` / `declaredNodeCount`. A budget is the ceiling at
32
+ which the author's fallback ending fires, not a denominator of progress.
33
+ - `endingId` is decided **server-side** from committed state and the package's
34
+ declared rules. No client, prompt or model supplies it.
35
+ - Server-side this is world API 1.26.0 — Story Contract v3 (`series`). Publishing
36
+ a v3 package needs `POUCHY_STORY_CONTRACT_V3_WRITE` on the deployment; until
37
+ then such a publish is refused with `code: "story_contract_v3_write_disabled"`.
38
+
39
+ ## 0.20.0
40
+
41
+ - **Seven methods for routes that already existed.** `getWorldOverview`,
42
+ `getWorldCost`, `listWorldVersions`, `listStoryPackageVersions`,
43
+ `setWorldDisabled`, `setStoryPackageDisabled` (all `adminKey`-capable), and
44
+ `preflightWorld` (owner plane only — no `/admin` mirror).
45
+ - **Two of them the README already promised.** Its credential table listed
46
+ `overview` and `cost` as `adminKey` reads, and there was no method to make
47
+ them with. `getWorldOverview` is the one to reach for first: it carries
48
+ `ready` and the `blockers` behind a false, and unlike `preflightWorld` a
49
+ machine can call it.
50
+ - **`conformance.mjs` no longer fails a correctly-configured project.** Its
51
+ header claimed an admin key was enough; two of its own gates call
52
+ `replayLedgerToEnd` and `createScriptDraft`, whose routes have no `/admin`
53
+ mirror, so on a key alone they threw "needs an owner token". Those gates now
54
+ SKIP with the reason printed, and skips are counted apart from passes rather
55
+ than folded into the ratio.
56
+ - No API change — `WORLD_API_VERSION` stays at 1.24.0. Every route these
57
+ methods call has been served on both planes since Batch 1; only the client
58
+ was missing.
59
+
3
60
  ## 0.19.0
4
61
 
5
62
  - `getApprovedExport(environmentId, worldInstanceId, draftId, editorialId, exportId)`
package/README.md CHANGED
@@ -52,7 +52,10 @@ directions a beat could take, then commit the one the player picked. Off unless
52
52
  the world's published revision declares it; the `envelope` is server-side only.
53
53
 
54
54
  **Reading** — `getWorldState`, `getProgress`, `getTurn`, `listTurns`,
55
- `listTurnsSince`, `replayLedger`, `replayLedgerToEnd`, `scanConsistency`.
55
+ `listTurnsSince`, `replayLedger`, `replayLedgerToEnd`, `scanConsistency`,
56
+ `getWorldOverview`, `getWorldCost`, `listWorldVersions`,
57
+ `listStoryPackageVersions`, `setWorldDisabled`, `setStoryPackageDisabled`,
58
+ `preflightWorld`.
56
59
 
57
60
  **Which credential does what.** This matters more than it looks: one of them
58
61
  expires within the hour.
@@ -64,9 +67,22 @@ expires within the hour.
64
67
  | `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop | outlive the hour |
65
68
 
66
69
  A server holds `secretKey` + `signing` + `adminKey` and needs **no browser login
67
- anywhere in its deployment** including `conformance.mjs`, which used to demand
68
- one. `adminToken` is for a person: the content loop's review step is a human
69
- decision, which is the point of it rather than an obstacle.
70
+ for the runtime loop**: author a story package and a world, mint sessions, drive
71
+ turns, and read everything back. `adminToken` is for a person, and the content
72
+ loop's review step is a human decision — the point of it rather than an obstacle.
73
+
74
+ **Which calls have no `/admin` mirror, and therefore need `adminToken`:** the
75
+ whole content loop (`*ScriptDraft*`, `*EditorialDraft*`, `createApprovedExport`,
76
+ `deriveStoryPackageCandidate`), the delivery-queue ACTIONS (`drainDeliveries`,
77
+ `requeueDelivery`, `rehydrateDelivery`, `resolveDeliveryGap`), `replayLedger`,
78
+ `archiveLedger`, `evaluateWorld`, and `preflightWorld`. Reading an approved
79
+ export back IS mirrored (`getApprovedExport`) — that is how a machine collects
80
+ what a person approved, with the id arriving on the `world.script_approved`
81
+ webhook.
82
+
83
+ `conformance.mjs` follows the same line: on an admin key alone it runs every
84
+ runtime gate and SKIPS the two whose routes are owner-plane, printing why.
85
+ Setting `POUCHY_ADMIN_TOKEN` as well runs all of them.
70
86
 
71
87
  ```ts
72
88
  const world = new PouchyWorldClient({
package/conformance.mjs CHANGED
@@ -94,12 +94,26 @@ console.log(`Pouchy World conformance — ${scenario.label} scenario\n`);
94
94
  const results = [];
95
95
  let failed = false;
96
96
 
97
+ /** Marks a gate that CANNOT run on the credentials in hand.
98
+ *
99
+ * Not a pass and not a failure. Two gates below drive the content loop and
100
+ * the ledger replay, and neither route has an `/admin` mirror — they are
101
+ * owner-plane by design ("creating stays human, reading does not"). Reporting
102
+ * them as FAIL would make a correctly-configured project look broken; leaving
103
+ * them out silently would make a partial run look complete. */
104
+ class NeedsOwnerToken extends Error {}
105
+
97
106
  async function check(name, fn) {
98
107
  try {
99
108
  const detail = await fn();
100
109
  results.push({ name, ok: true, detail });
101
110
  console.log(`PASS ${name}${detail ? ` — ${detail}` : ''}`);
102
111
  } catch (err) {
112
+ if (err instanceof NeedsOwnerToken) {
113
+ results.push({ name, ok: null, detail: err.message });
114
+ console.log(`SKIP ${name} — ${err.message}`);
115
+ return;
116
+ }
103
117
  failed = true;
104
118
  const detail =
105
119
  err instanceof WorldApiError
@@ -114,14 +128,21 @@ async function check(name, fn) {
114
128
 
115
129
  const world = new PouchyWorldClient({
116
130
  projectId: env('POUCHY_PROJECT_ID'),
117
- // Batch 10: an ADMIN KEY is enough, and is the point.
131
+ // Batch 10: an ADMIN KEY carries the RUNTIME loop, which is the point.
118
132
  //
119
133
  // This harness used to demand POUCHY_ADMIN_TOKEN — a Firebase ID token,
120
134
  // browser-minted and good for about an hour. So the one check that proves a
121
135
  // project's world plane is wired correctly could not run without a person
122
136
  // opening a browser first, which is exactly the thing the rest of this SDK
123
- // stopped requiring. Either credential works now; the key is preferred and
124
- // needs no human.
137
+ // stopped requiring.
138
+ //
139
+ // CORRECTED: "either credential works" was too strong, and this file was the
140
+ // proof — two of its own gates call `replayLedgerToEnd` and
141
+ // `createScriptDraft`, whose routes have NO `/admin` mirror. On an admin key
142
+ // alone they threw "needs an owner token", so the harness could not pass a
143
+ // correctly-configured project. Those two gates now SKIP with the reason
144
+ // printed; every runtime gate runs on the key. Set POUCHY_ADMIN_TOKEN as
145
+ // well to run all of them.
125
146
  ...(process.env.POUCHY_ADMIN_KEY
126
147
  ? { adminKey: process.env.POUCHY_ADMIN_KEY }
127
148
  : { adminToken: env('POUCHY_ADMIN_TOKEN') }),
@@ -133,6 +154,15 @@ const world = new PouchyWorldClient({
133
154
  },
134
155
  ...(process.env.POUCHY_BASE_URL ? { baseUrl: process.env.POUCHY_BASE_URL } : {})
135
156
  });
157
+ /** Throws a SKIP for a gate whose route is owner-plane only. */
158
+ function requireOwnerToken(route) {
159
+ if (!process.env.POUCHY_ADMIN_TOKEN) {
160
+ throw new NeedsOwnerToken(
161
+ `${route} has no /admin mirror — set POUCHY_ADMIN_TOKEN to run this gate`
162
+ );
163
+ }
164
+ }
165
+
136
166
  const agentA = env('POUCHY_AGENT_A');
137
167
  const agentB = env('POUCHY_AGENT_B');
138
168
  const stamp = Date.now().toString(36);
@@ -237,6 +267,7 @@ await check('world state reads back (user projection, no private layer)', async
237
267
  });
238
268
 
239
269
  await check('replay verifies the ledger against the served state', async () => {
270
+ requireOwnerToken('/ledger/replay');
240
271
  const report = await world.replayLedgerToEnd(created.environmentId, instanceId);
241
272
  if (report.verdict !== 'consistent') {
242
273
  throw new Error(`${report.verdict}${report.detail ? `: ${report.detail}` : ''}`);
@@ -246,6 +277,7 @@ await check('replay verifies the ledger against the served state', async () => {
246
277
  });
247
278
 
248
279
  await check('a script draft generates and refuses export before review', async () => {
280
+ requireOwnerToken('/script-drafts');
249
281
  const draft = await world.createScriptDraft(created.environmentId, instanceId);
250
282
  if (draft.content?.humanReviewRequired !== true) {
251
283
  throw new Error('draft did not carry humanReviewRequired');
@@ -284,9 +316,24 @@ await check('a WRONG signature is refused (the second proof is real)', async ()
284
316
  });
285
317
 
286
318
  console.log('');
287
- console.log(`${results.filter((r) => r.ok).length}/${results.length} checks passed`);
319
+ // SKIPPED is counted apart from PASSED, never folded into a ratio. A gate that
320
+ // could not run is not a gate that passed, and "8/10 passed" would read as two
321
+ // failures — the opposite of what happened.
322
+ const passed = results.filter((r) => r.ok === true).length;
323
+ const skipped = results.filter((r) => r.ok === null).length;
324
+ const ran = results.length - skipped;
325
+ console.log(
326
+ `${passed}/${ran} checks passed` + (skipped ? ` · ${skipped} skipped (no owner token)` : '')
327
+ );
288
328
  if (failed) {
289
329
  console.error('Conformance FAILED — fix the first failure above before writing product code.');
290
330
  process.exit(1);
291
331
  }
292
- console.log('Conformance passed. The world plane is wired correctly for this project.');
332
+ if (skipped) {
333
+ console.log(
334
+ `The RUNTIME plane is wired correctly. ${skipped} content-loop gate(s) did not run: ` +
335
+ `their routes are owner-plane only. Set POUCHY_ADMIN_TOKEN to cover them too.`
336
+ );
337
+ } else {
338
+ console.log('Conformance passed. The world plane is wired correctly for this project.');
339
+ }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * a project is running — which is exactly the field you reach for when a
5
5
  * customer's integration behaves like an older SDK than they say they have.
6
6
  * It sat at '0.1.0' for eight releases before anything compared the two. */
7
- export declare const WORLD_SDK_VERSION = "0.19.0";
7
+ export declare const WORLD_SDK_VERSION = "0.22.0";
8
8
  export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
9
9
  /** One direction a beat could take. The WHOLE of what a deliberation shows a
10
10
  * player: no reasoning, no role secrets, no simulated effects, no scores. */
@@ -216,8 +216,81 @@ export interface WorldProgressCheckpointV1 {
216
216
  branchId: string;
217
217
  condition: string;
218
218
  }>;
219
+ /** The episode this worldline is playing, when its pinned story is a
220
+ * serial. ABSENT for every non-serial world, which is most of them.
221
+ *
222
+ * `beatsCommitted` and `turnBudget` are two numbers, not a ratio — same
223
+ * rule as `completedNodeCount` / `declaredNodeCount`. A budget is the
224
+ * ceiling at which the author's fallback ending fires, not a denominator
225
+ * of progress.
226
+ *
227
+ * `endingId` appears only once the episode is over. It is decided
228
+ * server-side from committed state and the package's declared rules; no
229
+ * client, prompt or model supplies it. */
230
+ episode?: {
231
+ seriesId: string;
232
+ episodeId: string;
233
+ title: string;
234
+ status: 'active' | 'ended';
235
+ beatsCommitted: number;
236
+ turnBudget: number;
237
+ endingId?: string;
238
+ endingTitle?: string;
239
+ };
219
240
  rebuildable: true;
220
241
  }
242
+ /** What the next episode gets to stand on. A PROJECTION of facts the worldline
243
+ * already holds, not a copy of them — nothing is moved and nothing is deleted.
244
+ *
245
+ * Role-private notes are never here. Neither is memory: the world runtime
246
+ * writes none, so there is nothing to inherit. */
247
+ export interface WorldCarryoverV1 {
248
+ contractVersion: 1;
249
+ fromEpisodeId: string;
250
+ fromEndingId: string;
251
+ toEpisodeId: string;
252
+ /** Canon the previous episode established — ids AND the author's text. */
253
+ canonFacts: Array<{
254
+ factId: string;
255
+ text: string;
256
+ }>;
257
+ canonFactsTruncated: boolean;
258
+ /** Where the characters stand. Public by definition. */
259
+ relations: Array<{
260
+ between: [string, string];
261
+ descriptor: string;
262
+ }>;
263
+ relationsTruncated: boolean;
264
+ /** Declared nodes the previous episode never completed. */
265
+ unresolvedThreads: Array<{
266
+ nodeId: string;
267
+ objective: string;
268
+ }>;
269
+ unresolvedThreadsTruncated: boolean;
270
+ /** Declared facts never revealed — setups it did not pay off. Not called
271
+ * "promises": a promise is something a character made, which this system
272
+ * does not model. */
273
+ unfulfilledSetups: Array<{
274
+ factId: string;
275
+ text: string;
276
+ }>;
277
+ unfulfilledSetupsTruncated: boolean;
278
+ }
279
+ export interface WorldNextEpisodeResponse {
280
+ /** False on a retry that landed on the run already started. */
281
+ created: boolean;
282
+ episode: {
283
+ seriesId: string;
284
+ episodeId: string;
285
+ episodeRunId: string;
286
+ status: 'active' | 'ended';
287
+ startedAt: string;
288
+ startedAtRevision: number;
289
+ startedFromEpisodeId?: string;
290
+ startedFromEndingId?: string;
291
+ };
292
+ carryover: WorldCarryoverV1;
293
+ }
221
294
  export interface WorldTurnReadback {
222
295
  turnId: string;
223
296
  worldInstanceId: string;
@@ -892,6 +965,77 @@ export declare class PouchyWorldClient {
892
965
  contentHash: string;
893
966
  }>;
894
967
  getWorld(environmentId: string): Promise<unknown>;
968
+ /** The world's readiness and operational aggregate — what the dashboard's
969
+ * World Overview binds to.
970
+ *
971
+ * This is the call to make BEFORE driving anything: `ready` plus the
972
+ * `blockers` that explain a false, instance counts, and the delivery
973
+ * queue's shape. It was reachable on both planes from the start and simply
974
+ * had no method here, which meant the README's own credential table
975
+ * promised an `adminKey` read the SDK could not perform. */
976
+ getWorldOverview(environmentId: string): Promise<unknown>;
977
+ /** What this world has cost, by month.
978
+ *
979
+ * `months` is clamped server-side; omit it for the default window. */
980
+ getWorldCost(environmentId: string, months?: number): Promise<unknown>;
981
+ /** Every published revision of this world.
982
+ *
983
+ * An instance is pinned to the revision it was created on, so this is how a
984
+ * backend learns which definition a running story is actually playing. */
985
+ listWorldVersions(environmentId: string): Promise<{
986
+ versions: unknown[];
987
+ }>;
988
+ /** Every published revision of a story package.
989
+ *
990
+ * The response carries `hasUnsupported` / `unsupportedCount` as well as the
991
+ * versions: a history this build cannot read whole is reported as PARTIAL
992
+ * rather than passing its newest readable revision off as the newest one.
993
+ * Check that flag before treating the last entry as current. */
994
+ listStoryPackageVersions(packageId: string): Promise<{
995
+ versions: unknown[];
996
+ unsupported?: unknown;
997
+ unsupportedCount?: number;
998
+ hasUnsupported?: boolean;
999
+ }>;
1000
+ /** Stop (or restart) a world.
1001
+ *
1002
+ * A disabled world refuses new sessions and new turns. Existing instances
1003
+ * are not deleted and their ledgers are untouched — this is a door being
1004
+ * shut, not history being erased, and passing `false` opens it again. */
1005
+ setWorldDisabled(environmentId: string, disabled: boolean): Promise<{
1006
+ environmentId: string;
1007
+ disabled: boolean;
1008
+ }>;
1009
+ /** Stop (or restart) a story package.
1010
+ *
1011
+ * Worlds already PINNED to one of its revisions keep running: an instance
1012
+ * holds its own revision for life. This stops new pins. */
1013
+ setStoryPackageDisabled(packageId: string, disabled: boolean): Promise<{
1014
+ packageId: string;
1015
+ disabled: boolean;
1016
+ }>;
1017
+ /** The pre-launch checklist for a world: every reason it would refuse to
1018
+ * run, gathered before you try.
1019
+ *
1020
+ * OWNER TOKEN ONLY — there is no `/admin` mirror for this route, so an
1021
+ * unattended backend cannot call it. That is not an oversight to work
1022
+ * around: `getWorldOverview` carries `ready` and `blockers` and IS
1023
+ * mirrored, so a machine has the readiness answer it needs. */
1024
+ preflightWorld(environmentId: string): Promise<unknown>;
1025
+ /** Start the NEXT episode of a worldline that just finished one.
1026
+ *
1027
+ * The worldline does NOT restart. Its `worldInstanceId`, membership, state
1028
+ * and ledger all continue — that is the point — and this moves the episode
1029
+ * on and hands back the CARRYOVER: what the next episode gets to stand on.
1030
+ *
1031
+ * Idempotent. The run id is derived from the worldline, the episode and the
1032
+ * revision it starts at, so a retried call lands on the same run and
1033
+ * answers `created: false` rather than forking the story.
1034
+ *
1035
+ * OWNER TOKEN ONLY — no `/admin` mirror, on the same reasoning as
1036
+ * `preflightWorld`: the mirror carries reads, and this is the write that
1037
+ * decides a story moves on. */
1038
+ startNextEpisode(environmentId: string, worldInstanceId: string): Promise<WorldNextEpisodeResponse>;
895
1039
  /** Publish the next world revision. Existing world INSTANCES keep the
896
1040
  * revision they were created on — a published change reaches new instances
897
1041
  * only, which is what keeps a running story from changing runtime or rules
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import { createHash, createHmac, randomUUID } from 'node:crypto';
23
23
  * a project is running — which is exactly the field you reach for when a
24
24
  * customer's integration behaves like an older SDK than they say they have.
25
25
  * It sat at '0.1.0' for eight releases before anything compared the two. */
26
- export const WORLD_SDK_VERSION = '0.19.0';
26
+ export const WORLD_SDK_VERSION = '0.22.0';
27
27
  export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
28
28
  /** Read the commit turn id out of an envelope, so the signature covers the id
29
29
  * the server will commit under. The payload half is base64url JSON; the
@@ -287,6 +287,81 @@ export class PouchyWorldClient {
287
287
  getWorld(environmentId) {
288
288
  return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}`, `/admin/environments/${environmentId}`);
289
289
  }
290
+ /** The world's readiness and operational aggregate — what the dashboard's
291
+ * World Overview binds to.
292
+ *
293
+ * This is the call to make BEFORE driving anything: `ready` plus the
294
+ * `blockers` that explain a false, instance counts, and the delivery
295
+ * queue's shape. It was reachable on both planes from the start and simply
296
+ * had no method here, which meant the README's own credential table
297
+ * promised an `adminKey` read the SDK could not perform. */
298
+ getWorldOverview(environmentId) {
299
+ return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/overview`, `/admin/environments/${environmentId}/overview`);
300
+ }
301
+ /** What this world has cost, by month.
302
+ *
303
+ * `months` is clamped server-side; omit it for the default window. */
304
+ getWorldCost(environmentId, months) {
305
+ const query = months === undefined ? '' : `?months=${encodeURIComponent(String(months))}`;
306
+ return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/cost`, `/admin/environments/${environmentId}/cost`, undefined, query);
307
+ }
308
+ /** Every published revision of this world.
309
+ *
310
+ * An instance is pinned to the revision it was created on, so this is how a
311
+ * backend learns which definition a running story is actually playing. */
312
+ listWorldVersions(environmentId) {
313
+ return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}/versions`, `/admin/environments/${environmentId}/versions`);
314
+ }
315
+ /** Every published revision of a story package.
316
+ *
317
+ * The response carries `hasUnsupported` / `unsupportedCount` as well as the
318
+ * versions: a history this build cannot read whole is reported as PARTIAL
319
+ * rather than passing its newest readable revision off as the newest one.
320
+ * Check that flag before treating the last entry as current. */
321
+ listStoryPackageVersions(packageId) {
322
+ return this.machine('GET', `/projects/${this.projectId}/story-packages/${packageId}/versions`, `/admin/story-packages/${packageId}/versions`);
323
+ }
324
+ /** Stop (or restart) a world.
325
+ *
326
+ * A disabled world refuses new sessions and new turns. Existing instances
327
+ * are not deleted and their ledgers are untouched — this is a door being
328
+ * shut, not history being erased, and passing `false` opens it again. */
329
+ setWorldDisabled(environmentId, disabled) {
330
+ return this.machine('POST', `/projects/${this.projectId}/environments/${environmentId}/disable`, `/admin/environments/${environmentId}/disable`, { disabled });
331
+ }
332
+ /** Stop (or restart) a story package.
333
+ *
334
+ * Worlds already PINNED to one of its revisions keep running: an instance
335
+ * holds its own revision for life. This stops new pins. */
336
+ setStoryPackageDisabled(packageId, disabled) {
337
+ return this.machine('POST', `/projects/${this.projectId}/story-packages/${packageId}/disable`, `/admin/story-packages/${packageId}/disable`, { disabled });
338
+ }
339
+ /** The pre-launch checklist for a world: every reason it would refuse to
340
+ * run, gathered before you try.
341
+ *
342
+ * OWNER TOKEN ONLY — there is no `/admin` mirror for this route, so an
343
+ * unattended backend cannot call it. That is not an oversight to work
344
+ * around: `getWorldOverview` carries `ready` and `blockers` and IS
345
+ * mirrored, so a machine has the readiness answer it needs. */
346
+ preflightWorld(environmentId) {
347
+ return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/preflight`);
348
+ }
349
+ /** Start the NEXT episode of a worldline that just finished one.
350
+ *
351
+ * The worldline does NOT restart. Its `worldInstanceId`, membership, state
352
+ * and ledger all continue — that is the point — and this moves the episode
353
+ * on and hands back the CARRYOVER: what the next episode gets to stand on.
354
+ *
355
+ * Idempotent. The run id is derived from the worldline, the episode and the
356
+ * revision it starts at, so a retried call lands on the same run and
357
+ * answers `created: false` rather than forking the story.
358
+ *
359
+ * OWNER TOKEN ONLY — no `/admin` mirror, on the same reasoning as
360
+ * `preflightWorld`: the mirror carries reads, and this is the write that
361
+ * decides a story moves on. */
362
+ startNextEpisode(environmentId, worldInstanceId) {
363
+ return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/episodes/next`, {});
364
+ }
290
365
  /** Publish the next world revision. Existing world INSTANCES keep the
291
366
  * revision they were created on — a published change reaches new instances
292
367
  * only, which is what keeps a running story from changing runtime or rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.22.0",
4
4
  "description": "Server-side TypeScript client for Pouchy World \u2014 story packages, world definitions, world sessions, coordinated turns, trusted events, replay verification and script drafts. Node only: it holds a project Secret Key and a source signing key, which never belong in a browser or a mobile app.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",