@maka/maka-cli 5.90.0 → 5.92.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.
Files changed (115) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +9 -9
  3. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +54 -3
  5. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +987 -53
  6. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.d.ts +0 -14
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +23 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.d.ts +10 -2
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js +11 -2
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/breach.js.map +1 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +14 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js +30 -3
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/bypass-command.js.map +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +5 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +5 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +10 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +2 -2
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +2 -2
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +8 -1
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +3 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +2 -2
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +55 -17
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js +12 -0
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/project.js.map +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +14 -4
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +3 -3
  45. package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
  46. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +109 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +62 -16
  50. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +26 -3
  52. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +95 -3
  54. package/bundle/typescript/src/commands/game/sideQuest/game.js +237 -15
  55. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  56. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +5 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
  58. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.d.ts +13 -0
  59. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js +45 -2
  60. package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js.map +1 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +65 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +83 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +153 -3
  65. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +197 -12
  66. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +73 -0
  68. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +97 -3
  69. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +33 -1
  71. package/bundle/typescript/src/commands/game/sideQuest/qualities.js +80 -2
  72. package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
  73. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +1 -1
  74. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +4 -4
  75. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.d.ts +120 -0
  76. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js +135 -0
  77. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js.map +1 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +34 -2
  79. package/bundle/typescript/src/commands/game/sideQuest/ui.js +36 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  81. package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.d.ts +116 -0
  82. package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +127 -0
  83. package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js.map +1 -0
  84. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +1 -1
  85. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +123 -2
  86. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +131 -2
  88. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +170 -12
  89. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
  90. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +139 -1
  91. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +307 -5
  92. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
  93. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.d.ts +41 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +41 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js.map +1 -1
  96. package/bundle/typescript/src/commands/game/sideQuest/utilities/input-history.d.ts +98 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/utilities/input-history.js +137 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/utilities/input-history.js.map +1 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +1 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js.map +1 -1
  101. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.d.ts +38 -0
  102. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +106 -0
  103. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  104. package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js +19 -3
  105. package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js.map +1 -1
  106. package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +1 -1
  107. package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js.map +1 -1
  108. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +2 -1
  109. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  110. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +22 -0
  111. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +27 -0
  112. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
  113. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +79 -44
  114. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  115. package/package.json +1 -1
@@ -7,14 +7,15 @@ import { Item } from './models/item.js';
7
7
  import { Room } from './models/_index.js';
8
8
  import { Door } from './models/door.js';
9
9
  import { SAVE_VERSION } from './types/save-file.js';
10
- import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, restoreItem, readSave, serializeItem, saveSlug, } from './utilities/persistence.js';
10
+ import { serializePlayer, restorePlayer, captureHubOverlay, applyHubOverlay, writeSaveAtomic, deleteSave, restoreItem, readSave, serializeItem, saveSlug, spillPathFor, } from './utilities/persistence.js';
11
11
  import { heartbeatPresence, heartbeatPresenceNow } from './utilities/social.js';
12
12
  import { presenceBeatAllowed } from './utilities/presence-gate.js';
13
- import { pushSave, burnCloudSaveFor } from './utilities/cloud-saves.js';
13
+ import { pushSave, burnCloudSaveFor, enqueueBoundPush, flushPushQueue, writeSpill, clearSpill, hasPendingPush, } from './utilities/cloud-saves.js';
14
14
  import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
15
15
  import { isFence, LIFESTYLE_TIERS } from './utilities/commerce.js';
16
16
  import { bestFakeSin, wandRead, processArrest } from './utilities/sin.js';
17
17
  import { tickEphemerals } from './utilities/ephemeral.js';
18
+ import { requestAlarmBeat, newAlarmBeatLatch, ALARM_STIMULUS } from './utilities/alarmed-staff.js';
18
19
  import { renderMapViewport, mapCaption } from './utilities/map-view.js';
19
20
  import { tallestRoomRows } from './utilities/room-grid.js';
20
21
  import { renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
@@ -179,6 +180,16 @@ export default class Game {
179
180
  // Quit confirmation state (see handleInput / initInputBox): armed by
180
181
  // the first quit/exit or ESC, cleared by anything that isn't a yes.
181
182
  _quitArmed = false;
183
+ /**
184
+ * The quit verb already sealed this runner to the grid, so the
185
+ * process-exit hook has nothing left to do. Set ONLY after an awaited
186
+ * flush -- an optimistic set would make the exit hook skip the spill
187
+ * write on a push that never landed, which is the one way this design
188
+ * can silently lose a beat. (Mutation-checked: never setting it is
189
+ * caught, and so is setting it without the flush.)
190
+ */
191
+ _quitSealed = false;
192
+ get quitSealed() { return this._quitSealed; }
182
193
  _escArmedAt = 0;
183
194
  _statusBox;
184
195
  /** The clock gutter left of the prompt, its ticker, and the face it
@@ -2872,13 +2883,45 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2872
2883
  this.refreshLogLabel();
2873
2884
  Logger.getInstance().write(`Resumed save for ${save.playerName} (saved ${save.savedAt}, reason ${save.reason}).`);
2874
2885
  }
2886
+ /**
2887
+ * IS THIS RUNNER'S LIFE THE SERVER'S? (server-only storage.)
2888
+ *
2889
+ * A bound runner has an account behind them; an anonymous one never
2890
+ * leaves this machine, and a bench is a scratch session with nothing
2891
+ * worth keeping. Every new branch in the save path gates on this, so
2892
+ * the anonymous route is the code that was already here -- which is
2893
+ * what the unmodified tripwire suites prove.
2894
+ */
2895
+ get boundRunner() {
2896
+ return !this._anonymous && !this._bench && !!this._owner;
2897
+ }
2898
+ /** Where this runner's crash journal lives, when they are bound. */
2899
+ get spillPath() {
2900
+ if (!this._saveFilePath || !this.boundRunner)
2901
+ return undefined;
2902
+ return spillPathFor(dirname(this._saveFilePath), this.player.name);
2903
+ }
2875
2904
  /**
2876
2905
  * THE single save routine -- every safe beat funnels here. Guards make
2877
2906
  * it a silent no-op anywhere a save would lie: mid-run (the "runs are
2878
2907
  * disposable" ruling lives in the scene === _hubScene check), downed
2879
- * or KO'd, mid-exchange, dead, or before init finishes. Synchronous by
2880
- * design so the process-exit quit hook can use it; failures log and
2881
- * never crash a turn.
2908
+ * or KO'd, mid-exchange, dead, or before init finishes.
2909
+ *
2910
+ * SYNCHRONOUS BY DESIGN, AND FOR TWO DIFFERENT REASONS NOW. It always
2911
+ * had to be, because `process.on('exit')` can run nothing
2912
+ * asynchronous. For a BOUND runner that same constraint is what makes
2913
+ * the spill the durability guarantee rather than the push: the beat
2914
+ * writes `<slug>.spill` synchronously and returns, and the upload
2915
+ * happens behind it on a queue that may never get the chance to
2916
+ * finish. Nothing here awaits the grid, ever -- a save beat that could
2917
+ * block on a network is a turn that can hang.
2918
+ *
2919
+ * PR1 KEEPS WRITING `<slug>.json` FOR BOUND RUNNERS TOO. That is
2920
+ * deliberate and temporary: this PR is a superset of today's
2921
+ * behaviour, so reverting it strands no data and loses no character.
2922
+ * PR2 is where the bound branch stops writing the local slot, once
2923
+ * the server contract is deployed and the launcher can source the menu
2924
+ * from it.
2882
2925
  */
2883
2926
  requestSave(reason) {
2884
2927
  const logger = Logger.getInstance();
@@ -2903,17 +2946,74 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2903
2946
  save.sync = prev.save.sync;
2904
2947
  writeSaveAtomic(this._saveFilePath, save);
2905
2948
  logger.write(`Saved (${reason}) -> ${this._saveFilePath}`);
2906
- // Local-first: the beat is DONE the moment the file lands. The
2907
- // cloud mirror rides behind, fire-and-forget (see cloud-saves.ts
2908
- // -- logged-out/offline/expired all degrade to a silent no-op).
2909
- // An ANONYMOUS runner's life never leaves this machine.
2910
- if (!this._anonymous)
2911
- void pushSave(this._saveFilePath, save);
2949
+ // An ANONYMOUS runner's life never leaves this machine, and this
2950
+ // is where that stays true: no spill, no queue, no request.
2951
+ if (this._anonymous)
2952
+ return;
2953
+ const spill = this.spillPath;
2954
+ if (spill) {
2955
+ // THE JOURNAL, WRITTEN BEFORE THE PUSH IS EVEN ATTEMPTED. If the
2956
+ // grid is down -- or the process dies in the next millisecond --
2957
+ // this file is the beat. Ordering matters: enqueueing first and
2958
+ // spilling second would leave a window where a crash loses a
2959
+ // beat that a confirmed push had not yet covered.
2960
+ writeSpill(spill, save);
2961
+ enqueueBoundPush(this._saveFilePath, save, spill);
2962
+ return;
2963
+ }
2964
+ // Legacy/unbound-but-not-anonymous: the pre-existing mirror, still
2965
+ // fire-and-forget (cloud-saves.ts -- logged-out, offline and
2966
+ // expired all degrade to a silent no-op).
2967
+ void pushSave(this._saveFilePath, save);
2912
2968
  }
2913
2969
  catch (err) {
2914
2970
  logger.error(`requestSave(${reason}) failed: ${err}`);
2915
2971
  }
2916
2972
  }
2973
+ /**
2974
+ * SEAL A BOUND RUNNER TO THE GRID -- the awaited counterpart of
2975
+ * requestSave, for the two beats where "eventually" is a bug.
2976
+ *
2977
+ * QUIT: nothing runs after this process. An unpushed beat is not
2978
+ * "late", it is gone until the next launch sweeps the spill -- and
2979
+ * the player was told their game was saved.
2980
+ *
2981
+ * JOB ACCEPT: the shared run loads this character FROM THE SERVER.
2982
+ * An unpushed beat means the table plays a stale snapshot of them,
2983
+ * which is a real bug today (the unawaited push before shared runs)
2984
+ * and closes here for free.
2985
+ *
2986
+ * Returns true when the server has the newest save. FALSE IS NOT A
2987
+ * FAILURE PATH -- it means the spill stands and the next launch
2988
+ * uploads it, which is ruling 2 working exactly as designed. Callers
2989
+ * report, they do not block the player.
2990
+ *
2991
+ * Anonymous and bench runners return true immediately: they have
2992
+ * nothing to seal and must never be made to wait on a network.
2993
+ */
2994
+ async flushBoundSave(reason) {
2995
+ if (!this.boundRunner)
2996
+ return true;
2997
+ // Take a final beat first, so the thing we seal is the state the
2998
+ // player is quitting FROM rather than whatever the last autosave
2999
+ // happened to catch.
3000
+ this.requestSave(reason);
3001
+ if (!hasPendingPush())
3002
+ return true;
3003
+ const sealed = await flushPushQueue();
3004
+ if (sealed) {
3005
+ const spill = this.spillPath;
3006
+ // The queue clears the spill on a confirmed push; this is the
3007
+ // belt for the case where the confirmation raced the final beat.
3008
+ if (spill) {
3009
+ const current = readSave(this._saveFilePath);
3010
+ if (current.ok)
3011
+ clearSpill(spill, current.save.savedAt);
3012
+ }
3013
+ }
3014
+ Logger.getInstance().write(`flushBoundSave(${reason}): ${sealed ? 'sealed' : 'left in the spill'}.`);
3015
+ return sealed;
3016
+ }
2917
3017
  buildSaveFile(reason) {
2918
3018
  return {
2919
3019
  version: SAVE_VERSION,
@@ -3228,6 +3328,36 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3228
3328
  // Read the run's fate BEFORE the scene swap below discards it -- a
3229
3329
  // walked-away job gets a different homecoming line than a wrapped one.
3230
3330
  const wrappedRun = this.scene !== hub && this.scene.isCompleted();
3331
+ // A LOUD RUN PUTS YOUR FACE ON A FEED (TRqb2zBphwpDkeXP4, Maka: "the
3332
+ // alarm was exciting, but nothing came of it").
3333
+ //
3334
+ // Read here, BEFORE the scene swap below discards the run, for the
3335
+ // same reason wrappedRun is.
3336
+ //
3337
+ // PUBLIC AWARENESS, NOT NOTORIETY, and canon draws that line for us.
3338
+ // p.368 lists "leaving significant physical evidence" among the
3339
+ // things that raise AWARENESS; the notoriety list is about how you
3340
+ // treat the people you work with. A site left alarmed is a site that
3341
+ // knows it was hit -- a forced door, a tripped circuit, a report
3342
+ // filed. That is evidence, not unprofessionalism, and staining
3343
+ // notoriety instead would have eaten street cred in socialRep for
3344
+ // being NOTICED.
3345
+ //
3346
+ // ONE POINT PER RUN, NOT PER ALARM. Canon awards "a point" for the
3347
+ // actions of a run in the gamemaster's judgment, not a tick per
3348
+ // door -- and a three-door job is one story on one feed. The job
3349
+ // name is in the reason string, which is also the dedupe key, so a
3350
+ // second loud night earns its own point.
3351
+ //
3352
+ // NO PAYOUT IS DOCKED, deliberately. Canon has no rule reducing pay
3353
+ // for a noisy run -- p.373-374 payment modifiers only ever INCREASE
3354
+ // -- so the reported "the payout didn't get docked" is answered by
3355
+ // saying canon does not do that, and spending the consequence
3356
+ // somewhere the books actually put it.
3357
+ const loudRun = this.scene !== hub && this.scene.alarmRaised;
3358
+ const awarenessLine = loudRun && this.player.addPublicAwareness(`left "${this.currentJobName ?? 'a job'}" screaming`)
3359
+ ? `\nYou left the place howling behind you. Somebody filed a report, somebody kept a still -- your face is a little more public than it was this morning. (+1 Public Awareness, now ${this.player.publicAwareness})`
3360
+ : '';
3231
3361
  // THE CLIENT REMEMBERS (player ruling: the run's outcome is
3232
3362
  // reputation with the CLIENT, not the fixer).
3233
3363
  const clientLine = this.scene !== hub ? this.settleClientReputation(wrappedRun) : '';
@@ -3397,7 +3527,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3397
3527
  const fateLine = wrappedRun
3398
3528
  ? `The run's wrapped and the street swallows you back up.`
3399
3529
  : `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
3400
- return `${checkpointBlock ? `\n${checkpointBlock}\n` : ''}\n${this._hubName.toUpperCase()}\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}${clientLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}${allyLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
3530
+ return `${checkpointBlock ? `\n${checkpointBlock}\n` : ''}\n${this._hubName.toUpperCase()}\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}${clientLine}${awarenessLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}${allyLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
3401
3531
  }
3402
3532
  /**
3403
3533
  * KNIGHT ERRANT CHECKPOINT (SINs & getting caught, ruling 2026-08-24;
@@ -3520,7 +3650,21 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3520
3650
  const declined = scrubHandles(String(shared.declined), this.crew.map(m => ({ handle: m.ghost?.owner, name: m.name })));
3521
3651
  const skew = /runs engine ([\d.]+)/.exec(declined);
3522
3652
  sharedDeclineNote = skew
3523
- ? ` "Crew channel's scrambled tonight -- no job till it clears."\n{yellow-fg}(Out of character: engine mismatch -- this machine runs ${srMod?.engineVersion() ?? '?'}, the street runs ${skew[1]}. If yours is older: npm i -g @maka/maka-cli@latest, then relaunch. If newer, the street is still rolling out.){/yellow-fg}`
3653
+ // "maka update maka", NOT the npm incantation (fWedMP6fKkNoew4ff).
3654
+ // The sibling of this line -- the preflight skew warning in
3655
+ // sideQuest.sub.cmd.ts -- was corrected when that item was
3656
+ // filed; this one was missed, and it is the SAME message about
3657
+ // the SAME situation, so a player who hit the skew in play
3658
+ // rather than at launch still got taught the npm command.
3659
+ //
3660
+ // The npm line was not merely off-brand here, it was WRONG for
3661
+ // an entire install shape: a packaged-binary maka has no npm
3662
+ // global to upgrade, so `npm i -g` either fails or quietly
3663
+ // installs a SECOND maka beside the one they are running.
3664
+ // `maka update maka` is the only instruction true of both --
3665
+ // updateMakaCli branches on isPackaged() and either delegates
3666
+ // to npm or downloads and sha256-verifies the release asset.
3667
+ ? ` "Crew channel's scrambled tonight -- no job till it clears."\n{yellow-fg}(Out of character: engine mismatch -- this machine runs ${srMod?.engineVersion() ?? '?'}, the street runs ${skew[1]}. If yours is older: maka update maka, then relaunch. If newer, the street is still rolling out.){/yellow-fg}`
3524
3668
  : shared.code === 'unverified'
3525
3669
  // The LEADER's account is unvetted: Krow's refusal is
3526
3670
  // personal (fiction ruling 2026-08-24), and the OOC line
@@ -3821,7 +3965,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3821
3965
  // session's homecoming hands back to it.
3822
3966
  if (this.pendingSharedRun) {
3823
3967
  const { sessionID } = this.pendingSharedRun;
3824
- this.requestSave('job-accept');
3968
+ // AWAITED, unlike the solo departure below, because the shared
3969
+ // session loads this character FROM THE SERVER. A fire-and-forget
3970
+ // push here is the known stale-snapshot race: the table starts
3971
+ // playing a version of you that predates the beat you just took.
3972
+ // For an anonymous or unbound runner this returns immediately.
3973
+ await this.flushBoundSave('job-accept');
3825
3974
  try {
3826
3975
  const { SharedRunSession } = await import('./utilities/shared-run.js');
3827
3976
  const banner = await SharedRunSession.startAsLeader(this, sessionID);
@@ -3844,6 +3993,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3844
3993
  }
3845
3994
  // The departure save: hub state with the job still on offer -- what
3846
3995
  // a mid-run quit resumes to (the "runs are disposable" ruling).
3996
+ //
3997
+ // NOT awaited, unlike the shared-run branch above, and the asymmetry
3998
+ // is deliberate: nothing loads this save from the server on a solo
3999
+ // run, so there is no stale-snapshot race to close -- and awaiting a
4000
+ // push here would put a network round trip (up to TIMEOUT_MS) in
4001
+ // front of every departure for no gain. The spill covers the crash.
3847
4002
  this.requestSave('job-accept');
3848
4003
  // CARRY THE OFFER'S HAGGLE ACROSS THE TRAVEL, BUT ONLY THIS
3849
4004
  // OFFER'S (d4yoYPcRAtWpLtm9y: "I got a notice that I've already
@@ -4948,6 +5103,29 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4948
5103
  if (this._quitArmed) {
4949
5104
  this._quitArmed = false;
4950
5105
  if (/^(y|yes|quit|exit)$/i.test(verb)) {
5106
+ // THE SEAL. This is the last moment anything asynchronous can
5107
+ // run for this character -- ui.ts turns a null response into
5108
+ // process.exit(0), and the exit hook that follows can only write
5109
+ // the spill. So a bound runner's final beat is pushed HERE,
5110
+ // awaited, or it does not get pushed at all.
5111
+ //
5112
+ // The player is told, because a quit that pauses for a network
5113
+ // round trip with no explanation reads as a hang.
5114
+ if (this.boundRunner) {
5115
+ const logger = Logger.getInstance();
5116
+ logger.logWithColor(`Sealing your save to the grid...`, 'cyan');
5117
+ this._screen?.render();
5118
+ const sealed = await this.flushBoundSave('quit');
5119
+ if (!sealed) {
5120
+ // NOT an error, and worded so it does not read as one: the
5121
+ // spill is on disk and the next launch uploads it. Saying
5122
+ // nothing here would be the actual failure -- the player
5123
+ // would believe their evening reached the server.
5124
+ logger.logWithColor(`The grid didn't answer -- your progress is saved on this machine and will upload next launch.`, 'yellow');
5125
+ this._screen?.render();
5126
+ }
5127
+ this._quitSealed = true;
5128
+ }
4951
5129
  return out(null);
4952
5130
  }
4953
5131
  // fall through -- the answer was a real command; play on.
@@ -4998,7 +5176,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
4998
5176
  const command = CommandFactory.createCommand(verb, context);
4999
5177
  if (command) {
5000
5178
  const result = await command.execute(args);
5001
- return out(this.withEphemeralTurn(actor, this.withICTurn(actor, result)));
5179
+ return out(this.withEphemeralTurn(actor, this.withAlarmTurn(actor, this.withICTurn(actor, result))));
5002
5180
  }
5003
5181
  // Typo tolerance: two straight sessions bounced on slips like "searh"
5004
5182
  // and "maop". Reuse the same fuzzy matcher item/NPC names go through
@@ -5057,6 +5235,50 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5057
5235
  * utilities/ephemeral.ts decides what each kind does with its beat;
5058
5236
  * this only carries the narration back. Only a mob ever produces any.
5059
5237
  */
5238
+ /** One beat in flight at a time (see utilities/alarmed-staff.ts). */
5239
+ _alarmBeat = newAlarmBeatLatch();
5240
+ /**
5241
+ * THE ALARMED SITE'S TURN, hung off the same single dispatch the host's
5242
+ * and the ephemerals' are -- one player action is one beat for the
5243
+ * people who work here.
5244
+ *
5245
+ * AN ENGINE MAPPING, NOT A RULE, in exactly the sense withICTurn says
5246
+ * it: SR5 writes no general mechanics for how guards react to an alarm
5247
+ * (asked twice, two wordings, same answer), and p.352's "delay the
5248
+ * runner team" is conditioned on an HTR team being en route, which in
5249
+ * a Zone 0 barrens it never is. Nothing here invents a response table,
5250
+ * a convergence rate or an aggression dial. What changed is only this:
5251
+ * NPC.act()'s two callers both live inside respondTo(), so a scene NPC
5252
+ * was mute unless the player addressed it. The engine stops muzzling
5253
+ * them while a site is alarmed. What any of them DOES with a beat is
5254
+ * the judgment it already exercises when spoken to.
5255
+ *
5256
+ * IT APPENDS NOTHING, and that is worth saying because it makes the
5257
+ * one thing this wrapper's POSITION was chosen for moot. The seam
5258
+ * agreement put this inner to withEphemeralTurn so an alarmed guard
5259
+ * would print above somebody eating noodles -- sound reasoning for an
5260
+ * appending wrapper, and irrelevant to a fire-and-forget one that
5261
+ * returns `result` untouched. The position is kept as agreed so the
5262
+ * slot exists if this ever narrates inline; the ordering argument is
5263
+ * recorded as not currently biting rather than quietly inherited.
5264
+ *
5265
+ * FIRE AND FORGET on the player's own ruling, offered three shapes:
5266
+ * reactions "arrive a moment later" while play continues at full
5267
+ * speed, one in flight so they never pile up. The precedent is
5268
+ * draftNextSceneInBackground -- never make the player wait on a model
5269
+ * call.
5270
+ */
5271
+ withAlarmTurn(actor, result) {
5272
+ if (result === null)
5273
+ return null;
5274
+ // The primary's actions drive the site. A second human's commands
5275
+ // arrive through the same dispatch, and two humans each triggering a
5276
+ // beat would double the site's voice without doubling the site.
5277
+ if (actor !== this.player)
5278
+ return result;
5279
+ requestAlarmBeat(this._alarmBeat, this.scene, actor, npc => npc.respondTo(actor, ALARM_STIMULUS));
5280
+ return result;
5281
+ }
5060
5282
  withEphemeralTurn(actor, result) {
5061
5283
  if (result === null)
5062
5284
  return null;