@maka/maka-cli 5.162.0 → 5.164.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.162.0",
3
+ "version": "5.164.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -1,6 +1,7 @@
1
1
  import { Command } from './command.js';
2
2
  import { hint } from '../utilities/hints.js';
3
3
  import { authToken } from '../utilities/cloud-saves.js';
4
+ import { currentSession } from '../utilities/session-context.js';
4
5
  import { submitBacklog, fetchBacklog, postBacklogComment, postBacklogSignoff, postBacklogReject, postBacklogClose, BACKLOG_STATUSES, } from '../utilities/backlog.js';
5
6
  import { noteBacklogOutcome } from '../utilities/playtester.js';
6
7
  import { takeIndex } from '../utilities/fuzzy-match.js';
@@ -163,6 +164,10 @@ export class BacklogCommand extends Command {
163
164
  cliVersion: cliVersion(),
164
165
  engineVersion: engineVersion(),
165
166
  platform: process.platform,
167
+ // The host names the client (session-context.ts): a browser run's
168
+ // engine is the same code on a linux box, and 'linux' told nobody
169
+ // where the report actually came from (wQEyefjzbwtdmdrJo).
170
+ client: currentSession()?.client ?? 'cli',
166
171
  });
167
172
  // A real call classifies the account for free -- no second request.
168
173
  noteBacklogOutcome(outcome);
@@ -408,6 +408,39 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
408
408
  }
409
409
  }
410
410
  }
411
+ // Step 4.95: NO DOOR SHIPS LOCKED WITHOUT A WAY THROUGH
412
+ // (S8WdW7gm9tFGaxeCi, 2026-09-07: "I broke the gate, but the door is
413
+ // locked. It doesn't look like it has a way to open it"). A seed
414
+ // exit's `keyRequired` builds a locked Door with no Device: "pick",
415
+ // "breach" and "hack" all answer "nothing here", the refusal names
416
+ // no route, and the only verb that works ("unlock <dir>" while
417
+ // carrying the key) was never surfaced. The device path was
418
+ // hardened against exactly this (seed-migration synthesises a code
419
+ // rather than ship a routeless barrier); the keyed path never was.
420
+ //
421
+ // Same clamp-don't-throw rule as the keyItem repair above: a key
422
+ // that resolves (exactly or by the same fuzzy match) is written back
423
+ // under its real name so "unlock" finds it; a key that resolves to
424
+ // nothing means the door cannot be opened by anyone, ever, and it
425
+ // ships UNLOCKED with the error logged. Runs on every seed path --
426
+ // static, generated, repro, and the hub seed inside every save.
427
+ for (const roomJson of json.rooms) {
428
+ const room = scene.getRoom(roomJson.name);
429
+ if (!room)
430
+ continue;
431
+ for (const [direction, exit] of room.exits.entries()) {
432
+ if (!(exit instanceof Door) || !exit.checkIfLocked() || exit.heldBy)
433
+ continue;
434
+ const wanted = exit.unlockItemOrSolution;
435
+ const key = wanted ? findItem(wanted) : undefined;
436
+ if (key) {
437
+ exit.unlockItemOrSolution = key.name;
438
+ continue;
439
+ }
440
+ exit.isLocked = false;
441
+ logger.error(`Scene "${json.name}": the door ${String(direction)} of ${room.name} was locked by "${wanted ?? '(nothing)'}", which no device holds and no item matches -- it ships unlocked rather than as a wall.`);
442
+ }
443
+ }
411
444
  // Step 4.05: NPCs wear their gear. "heldBy" only puts items in an NPC's
412
445
  // inventory, but combat pools (see Player.getWeaponProfile /
413
446
  // getArmorValue) read from EQUIPPED slots -- without this, an enforcer
@@ -2380,6 +2380,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2380
2380
  * unchanged.
2381
2381
  */
2382
2382
  _conditionSink;
2383
+ /** A hosted run's aftermath still owed to this hub (IPendingHomecoming). */
2384
+ _pendingHomecoming;
2383
2385
  constructor(player, sceneSeed, conditionSink) {
2384
2386
  this._conditionSink = conditionSink;
2385
2387
  this.player = player;
@@ -3043,6 +3045,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3043
3045
  // party-loadable within moments of launch now -- requestSave pushes
3044
3046
  // the cloud mirror behind the local write.
3045
3047
  if (this._hubScene && this.scene === this._hubScene) {
3048
+ // A HOSTED RUN'S AFTERMATH, SETTLED AT THE DOOR (8izFwokbehjHzkEAn,
3049
+ // 3gFcHfheGSMPtpCFh): the site wrote the runner back with a
3050
+ // homecoming owed; this is the hub entry that pays it.
3051
+ const owed = this.applyHostedHomecoming();
3052
+ if (owed)
3053
+ Logger.getInstance().logWithColor(owed, 'green');
3046
3054
  this.requestSave('hub-arrival');
3047
3055
  }
3048
3056
  // A REGISTERED sprite from the save re-shells at the cot (the
@@ -3184,6 +3192,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3184
3192
  }
3185
3193
  applyHubOverlay(this.scene, save.hub, this.player, this._homeRoom);
3186
3194
  this._tier = save.game.tier;
3195
+ this._pendingHomecoming = save.game.pendingHomecoming;
3187
3196
  this.contacts = new Map(save.game.contacts);
3188
3197
  // Roles are DERIVED, not earned -- re-derive them all on every
3189
3198
  // resume so old saves self-heal (pre-role saves read all "Street
@@ -3469,6 +3478,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3469
3478
  return all.length > 0 ? all : undefined;
3470
3479
  })(),
3471
3480
  crew: this.crew.map(m => ({ ...m, gear: [...m.gear] })),
3481
+ pendingHomecoming: this._pendingHomecoming,
3472
3482
  commandRefusals: { ...this.commandRefusals },
3473
3483
  fencedCategories: [...this.fencedCategories],
3474
3484
  rumorIndex: this._rumorIndex,
@@ -3843,6 +3853,50 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3843
3853
  contact.asked = false;
3844
3854
  // Fresh material for the buskers too (commands/perform.ts).
3845
3855
  this.performedThisCycle = false;
3856
+ // LIFESTYLE upkeep and the High-lifestyle shower: settleLifestyleUpkeep,
3857
+ // shared with the hosted run's owed homecoming (applyHostedHomecoming).
3858
+ const { rentLine, comfortLine } = this.settleLifestyleUpkeep();
3859
+ // Fresh streets, fresh gossip, restocked shelves, new work.
3860
+ this.advanceRumor();
3861
+ this.fenceSellsOn();
3862
+ const restocked = this.restockVendorShelves();
3863
+ const restockLine = restocked > 0 ? `\nThe district has restocked around you -- the wok steams, the bar's shelf is full, the market stalls are loud again.` : '';
3864
+ this.advanceGig();
3865
+ this.repaintForScene();
3866
+ this.refreshLogLabel();
3867
+ Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
3868
+ // Drones spin back up beside the cot.
3869
+ const companionArrivalLines = this.unfoldCompanionsOnArrival();
3870
+ const companionBlock = [...companionFoldLines, ...companionArrivalLines];
3871
+ // The commanded ally's tour ends with the run (RAW's Command lasts
3872
+ // turns; ours lasts the job): the crew disbands at the curb.
3873
+ // The crew stays hired between jobs (the roster persists); their
3874
+ // homecoming lines (obituaries, write-back) rode in above. Fresh
3875
+ // faces on the for-hire board each homecoming.
3876
+ this.rollHireCandidates();
3877
+ // Social settlement: the street hears you're home.
3878
+ this.beatPresence();
3879
+ const allyLine = crewLines.length > 0 ? `\n${crewLines.join('\n')}` : '';
3880
+ const docwagonLine = this.tickDocwagonHomecoming();
3881
+ const deliveryLine = this.deliverProcurements();
3882
+ // The night catches up on the ride (SINs & getting caught): a hot
3883
+ // run means the wire between there and home. Settled AFTER the
3884
+ // payout (the fee comes out of tonight's take) and BEFORE the save
3885
+ // below -- a burned fake or an issued SIN must survive a quit.
3886
+ const checkpointBlock = this.checkpointOnRideHome();
3887
+ // The homecoming save: everything above (payout, rent, restock,
3888
+ // contacts reset, deliveries, the checkpoint) settled first, so the
3889
+ // save reflects the hub as the player will find it.
3890
+ this.requestSave('homecoming');
3891
+ const fateLine = wrappedRun
3892
+ ? `The run's wrapped and the street swallows you back up.`
3893
+ : `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
3894
+ 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')}` : ''}`;
3895
+ }
3896
+ /** The rent, the slide, the tab, and the High-lifestyle shower --
3897
+ * homecoming's money half, for the solo ride home and the hosted
3898
+ * run's owed homecoming alike. */
3899
+ settleLifestyleUpkeep() {
3846
3900
  // LIFESTYLE upkeep comes due with the homecoming: this tier's share
3847
3901
  // plus any standing debt, paid from carried cred as far as it goes.
3848
3902
  // Coming up short SLIDES YOU DOWN a tier (the district notices) and
@@ -3887,75 +3941,98 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3887
3941
  const cleared = this.player.healStun(this.player.stunTaken);
3888
3942
  comfortLine = `\nThe shower runs hot until the run is a rumor -- ${cleared} stun gone before your head hits the pillow.`;
3889
3943
  }
3890
- // Fresh streets, fresh gossip, restocked shelves, new work.
3944
+ return { rentLine, comfortLine };
3945
+ }
3946
+ /** DOCWAGON's year ticks in homecomings (12 = a street year, player
3947
+ * ruling): the contract lapses quietly unless renewed at Doc's. */
3948
+ tickDocwagonHomecoming() {
3949
+ if (!this.docwagon)
3950
+ return '';
3951
+ this.docwagon.homecomingsLeft -= 1;
3952
+ if (this.docwagon.homecomingsLeft <= 0) {
3953
+ const tier = this.docwagon.tier;
3954
+ this.docwagon = undefined;
3955
+ return `\nYour DocWagon ${tier} contract LAPSES -- the wristband goes dark.${hint(` ("docwagon <tier>" at Doc's renews it.)`)}`;
3956
+ }
3957
+ if (this.docwagon.homecomingsLeft <= 2) {
3958
+ return `\nDocWagon renewal coming due: ${this.docwagon.homecomingsLeft} homecoming${this.docwagon.homecomingsLeft === 1 ? '' : 's'} left on the ${this.docwagon.tier} contract.`;
3959
+ }
3960
+ return '';
3961
+ }
3962
+ /** SWAG deliveries (commands/source.ts): the contact's shopping run
3963
+ * lands with the homecoming -- straight into your gear, paid for at
3964
+ * order time. */
3965
+ deliverProcurements() {
3966
+ if (this.pendingProcurements.length === 0)
3967
+ return '';
3968
+ const delivered = [];
3969
+ for (const order of this.pendingProcurements.splice(0)) {
3970
+ try {
3971
+ const item = restoreItem(order.itemConfig);
3972
+ item.owner = this.player.name;
3973
+ this.player.inventory.addItem(item);
3974
+ delivered.push(`${item.name} (${order.contact} came through)`);
3975
+ }
3976
+ catch (err) {
3977
+ Logger.getInstance().error(`Procurement delivery failed: ${err}`);
3978
+ }
3979
+ }
3980
+ if (delivered.length === 0)
3981
+ return '';
3982
+ this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
3983
+ return `\nWaiting with your gear: ${delivered.join(', ')}.`;
3984
+ }
3985
+ /**
3986
+ * A HOSTED RUN'S HOMECOMING (8izFwokbehjHzkEAn, 3gFcHfheGSMPtpCFh): the
3987
+ * aftermath returnToHub settles for a solo ride home, applied ONCE for
3988
+ * a run that ended on the site -- from the flag the server left on the
3989
+ * save (IPendingHomecoming), or handed in directly by a CLI that rode a
3990
+ * shared table. What it settles: the loud-run awareness point, the
3991
+ * Johnson's opinion, the crew's tally, contacts' legwork, rent and the
3992
+ * slide, the shower, restock/rumor/gig, the hire board, the DocWagon
3993
+ * year, deliveries. What it does not: the checkpoint on the ride home
3994
+ * (heat is the run's, and the run is gone) and crew gear write-back
3995
+ * (their shells never stood in this process). Clears the flag and
3996
+ * writes the homecoming save. Undefined when nothing is owed, or when
3997
+ * the player is not standing in the hub.
3998
+ */
3999
+ applyHostedHomecoming(info) {
4000
+ const owed = info ?? this._pendingHomecoming;
4001
+ this._pendingHomecoming = undefined;
4002
+ if (!owed)
4003
+ return undefined;
4004
+ if (!this._hubScene || this.scene !== this._hubScene)
4005
+ return undefined;
4006
+ const awarenessLine = owed.alarmed && this.player.addPublicAwareness(`left "${owed.jobName ?? 'a job'}" screaming`)
4007
+ ? `\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})`
4008
+ : '';
4009
+ this.currentClient = owed.client ? { ...owed.client } : undefined;
4010
+ const clientLine = this.settleClientReputation(owed.wrapped);
4011
+ for (const member of this.crew) {
4012
+ if (owed.wrapped && !member.ghost?.liveOnly)
4013
+ member.jobsRun += 1;
4014
+ member.trainedThisCycle = false;
4015
+ }
4016
+ for (const contact of this.contacts.values())
4017
+ contact.asked = false;
4018
+ this.performedThisCycle = false;
4019
+ const { rentLine, comfortLine } = this.settleLifestyleUpkeep();
3891
4020
  this.advanceRumor();
3892
4021
  this.fenceSellsOn();
3893
4022
  const restocked = this.restockVendorShelves();
3894
4023
  const restockLine = restocked > 0 ? `\nThe district has restocked around you -- the wok steams, the bar's shelf is full, the market stalls are loud again.` : '';
3895
4024
  this.advanceGig();
3896
- this.repaintForScene();
3897
- this.refreshLogLabel();
3898
- Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${paid} paid, ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
3899
- // Drones spin back up beside the cot.
3900
- const companionArrivalLines = this.unfoldCompanionsOnArrival();
3901
- const companionBlock = [...companionFoldLines, ...companionArrivalLines];
3902
- // The commanded ally's tour ends with the run (RAW's Command lasts
3903
- // turns; ours lasts the job): the crew disbands at the curb.
3904
- // The crew stays hired between jobs (the roster persists); their
3905
- // homecoming lines (obituaries, write-back) rode in above. Fresh
3906
- // faces on the for-hire board each homecoming.
3907
4025
  this.rollHireCandidates();
3908
- // Social settlement: the street hears you're home.
3909
- this.beatPresence();
3910
- const allyLine = crewLines.length > 0 ? `\n${crewLines.join('\n')}` : '';
3911
- // DOCWAGON's year ticks in homecomings (12 = a street year, player
3912
- // ruling): the contract lapses quietly unless renewed at Doc's.
3913
- let docwagonLine = '';
3914
- if (this.docwagon) {
3915
- this.docwagon.homecomingsLeft -= 1;
3916
- if (this.docwagon.homecomingsLeft <= 0) {
3917
- docwagonLine = `\nYour DocWagon ${this.docwagon.tier} contract LAPSES -- the wristband goes dark.${hint(` ("docwagon <tier>" at Doc's renews it.)`)}`;
3918
- this.docwagon = undefined;
3919
- }
3920
- else if (this.docwagon.homecomingsLeft <= 2) {
3921
- docwagonLine = `\nDocWagon renewal coming due: ${this.docwagon.homecomingsLeft} homecoming${this.docwagon.homecomingsLeft === 1 ? '' : 's'} left on the ${this.docwagon.tier} contract.`;
3922
- }
3923
- }
3924
- // SWAG deliveries (commands/source.ts): the contact's shopping run
3925
- // lands with the homecoming -- straight into your gear, paid for at
3926
- // order time.
3927
- let deliveryLine = '';
3928
- if (this.pendingProcurements.length > 0) {
3929
- const delivered = [];
3930
- for (const order of this.pendingProcurements.splice(0)) {
3931
- try {
3932
- const item = restoreItem(order.itemConfig);
3933
- item.owner = this.player.name;
3934
- this.player.inventory.addItem(item);
3935
- delivered.push(`${item.name} (${order.contact} came through)`);
3936
- }
3937
- catch (err) {
3938
- Logger.getInstance().error(`Procurement delivery failed: ${err}`);
3939
- }
3940
- }
3941
- if (delivered.length > 0) {
3942
- this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
3943
- deliveryLine = `\nWaiting with your gear: ${delivered.join(', ')}.`;
3944
- }
3945
- }
3946
- // The night catches up on the ride (SINs & getting caught): a hot
3947
- // run means the wire between there and home. Settled AFTER the
3948
- // payout (the fee comes out of tonight's take) and BEFORE the save
3949
- // below -- a burned fake or an issued SIN must survive a quit.
3950
- const checkpointBlock = this.checkpointOnRideHome();
3951
- // The homecoming save: everything above (payout, rent, restock,
3952
- // contacts reset, deliveries, the checkpoint) settled first, so the
3953
- // save reflects the hub as the player will find it.
4026
+ const docwagonLine = this.tickDocwagonHomecoming();
4027
+ const deliveryLine = this.deliverProcurements();
4028
+ Logger.getInstance().write(`Hosted homecoming applied (${owed.outcome}, ended ${owed.endedAt}): rent ${this.rentDebt} owed, lifestyle ${this.lifestyleTier}.`);
3954
4029
  this.requestSave('homecoming');
3955
- const fateLine = wrappedRun
4030
+ const fateLine = owed.outcome === 'wrapped'
3956
4031
  ? `The run's wrapped and the street swallows you back up.`
3957
- : `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
3958
- 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')}` : ''}`;
4032
+ : owed.outcome === 'interrupted'
4033
+ ? `The run was lost to the static -- the street doesn't ask.`
4034
+ : `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
4035
+ return `${fateLine} The table's settled; home takes its cut.${clientLine}${awarenessLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}`;
3959
4036
  }
3960
4037
  /**
3961
4038
  * KNIGHT ERRANT CHECKPOINT (SINs & getting caught, ruling 2026-08-24;
@@ -5307,6 +5384,15 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
5307
5384
  const heat = this.heatBand();
5308
5385
  if (heat && player === this.player)
5309
5386
  lines.push(heat);
5387
+ // THE ALARM (kqrSCWjj96SP4MRXj, web run 2026-09-11: "I got a
5388
+ // notification that the alarms sound, but I don't see any
5389
+ // indication in my status bar"). The site going loud was a world
5390
+ // event that scrolled away; Scene.alarmRaised has been the standing
5391
+ // fact since 2026-08-27, and it belongs on the HUD for as long as it
5392
+ // holds. A fact about the PLACE, so every player at the table sees
5393
+ // it -- unlike heat, which is the primary's own ledger.
5394
+ if (this.scene?.alarmRaised)
5395
+ lines.push(`{red-fg}🚨 ALARM -- the site is lit{/red-fg}`);
5310
5396
  // Line 3: the rest of the readiness read -- armor, and whether the
5311
5397
  // commlink is actually live (equipped), the recurring "why can't I
5312
5398
  // call anyone" friction surfaced at a glance. The armed state used
@@ -190,7 +190,7 @@ export function buildPlayerFromSave(save) {
190
190
  }
191
191
  export async function createHeadlessSession(opts) {
192
192
  const logger = new EmitterLogger(opts.sink, opts.debug);
193
- const ctx = { sessionId: opts.sessionId, logger };
193
+ const ctx = { sessionId: opts.sessionId, logger, client: opts.client };
194
194
  return runInSession(ctx, async () => {
195
195
  const primary = buildPlayerFromSave(opts.players[0]);
196
196
  const game = new Game(primary, opts.seed, opts.conditionSink);
@@ -261,6 +261,26 @@ export async function createHeadlessSession(opts) {
261
261
  const clientSpot = clientActor ? spotOf(clientActor) : undefined;
262
262
  logger.log(`📞 Your link chirps -- {bold}{underline}${clientName}{/underline}{/bold}: "${room ? `I'm at ${room}${clientSpot ? `, the ${clientSpot}` : ''}` : `I'm on-site`} -- waiting on you. Come sit down before somebody else does."`, 'all');
263
263
  }
264
+ // THE CREW RIDES ALONG (aCjJiDDJM9jGZJeER, 2026-09-11: "I can't see
265
+ // Hammerhead Joe doing any combat"). Game.spawnCrew has exactly one
266
+ // caller -- the SOLO arrival (activateRunScene) -- and this boot
267
+ // loaded only the leader's DocWagon wristband out of save.game, so a
268
+ // hosted run had no crew at all: a hired face the player had paid
269
+ // for simply was not in the scene. Same filter as Game's own save
270
+ // load (a friend invite rides live or not at all; a dead ghost
271
+ // dissolves), same spawn, same arrival lines, for the primary only
272
+ // -- a late joiner's crew is a table question this does not answer.
273
+ try {
274
+ const primarySave = opts.players[0];
275
+ game.crew = (primarySave?.game?.crew ?? [])
276
+ .filter(m => !(m.ghost && !m.ghost.liveOnly))
277
+ .map(m => ({ ...m, gear: [...m.gear] }));
278
+ for (const line of game.spawnCrew())
279
+ logger.log(line, 'all');
280
+ }
281
+ catch (err) {
282
+ logger.write(`spawnCrew (hosted boot) failed: ${err instanceof Error ? err.message : String(err)}`);
283
+ }
264
284
  const spawnRoom = () => game.scene.determineStartRoom();
265
285
  // PER-PLAYER MAP EVENTS (map-overlay wave 2026-08-24): each rider
266
286
  // gets their own icon minimap -- the client holds no Room objects
@@ -368,6 +388,14 @@ export async function createHeadlessSession(opts) {
368
388
  const res = await game.handleInputAs(actorName, input);
369
389
  session.pushStatus();
370
390
  pushMap();
391
+ // THE WEB MAP FOLLOWS THE ROOM, NOT JUST YOUR OWN BODY
392
+ // (oHhjxjqKuptDGMHLD: a looted body stayed on the map "until I
393
+ // move"; 9aEMA4APbB6rYtWnT: a killed NPC's dot never changed).
394
+ // The condition beat is what the browser draws actors from, and
395
+ // it fires only on the primary's OWN mutations -- a kill or a
396
+ // loot mutates the room, not the killer, so nothing fired until
397
+ // their next step. One nudge per command; the beat coalesces.
398
+ game.player.noteConditionChanged();
371
399
  return res;
372
400
  })),
373
401
  addPlayer: (save) => runInSession(ctx, async () => {
@@ -489,6 +517,7 @@ export async function createHeadlessSession(opts) {
489
517
  game.onSceneChanged = () => {
490
518
  session.pushStatus();
491
519
  pushMap();
520
+ game.player.noteConditionChanged();
492
521
  };
493
522
  return session;
494
523
  });
@@ -236,23 +236,44 @@ export function buildConditionReport(player, deps) {
236
236
  // guessed role): a construct on the matrix plane -- IC, sprite, agent --
237
237
  // has no body in the room. Another player's body still shows even
238
238
  // while their persona rides the grid.
239
+ // DOWNED, NOT DROPPED (9aEMA4APbB6rYtWnT, 2026-09-11: "when I kill
240
+ // an NPC, their icon on the map should change"): the room panel
241
+ // draws somebody face-down as ☓; this beat used to drop them, so
242
+ // the web dot simply vanished -- or, worse, stood there until the
243
+ // next beat. They ride with kind 'downed' now.
239
244
  actors: (player.currentLocation ? visibleActorsIn(player.currentLocation, player) : [])
240
245
  .filter(a => !(a.plane === 'matrix' && a !== player && (a.icHost !== undefined || a.companionKind === 'sprite' || a.companionKind === 'agent')))
241
- .filter(a => a !== player && !a.isIncapacitated())
246
+ .filter(a => a !== player)
242
247
  .slice(0, 24)
243
248
  .map(a => ({
244
249
  name: a.name,
245
250
  // THE SAME RED THE ROOM PANEL USES (see IConditionReportDeps
246
251
  // .isHostile): stance flag OR live enmity, and never a
247
252
  // surrendered one -- hostileByStance owns that clause.
248
- kind: a.allyOf ? 'ally'
249
- : (isHostile?.(a) ?? hostileByStance(a)) ? 'hostile' : 'neutral',
253
+ kind: a.isIncapacitated() ? 'downed'
254
+ : a.allyOf ? 'ally'
255
+ : (isHostile?.(a) ?? hostileByStance(a)) ? 'hostile' : 'neutral',
250
256
  role: actorRole(a),
251
257
  spot: a.atSpot,
252
258
  cell: a.atCell ?? seats.get(a.name),
253
259
  })),
260
+ // THE DEAD (same report): the room's bodies, where they fell -- the
261
+ // panel's 💀. Gone from the list the moment a body is picked clean
262
+ // (loot.ts removeBody), which with the per-command beat is at once.
263
+ bodies: roomBodies(player),
254
264
  };
255
265
  }
266
+ function roomBodies(player) {
267
+ const room = player.currentLocation;
268
+ if (!room || (player.plane !== 'meat' && player.plane !== 'drone'))
269
+ return [];
270
+ try {
271
+ return room.bodies.slice(0, 24).map(b => ({ name: b.name, cell: { x: b.cell.x, y: b.cell.y, z: b.cell.z } }));
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ }
256
277
  let lastRoster = '';
257
278
  /** One session-log line whenever the room or the roster changes -- the
258
279
  * cheapest possible answer to "what did the engine send?" */
@@ -276,7 +297,7 @@ function noteRoster(report) {
276
297
  const ADDITIVE_FIELDS = [
277
298
  'ar', 'actors', 'deckLine', 'deckDamage', 'astralPerceiving', 'bodyRoom', 'sustainingPenalty',
278
299
  'matrix', 'panMarks', 'hostMarksOnYou', 'marksPlaced', 'marksOnYou', 'deckStored', 'droneDamage', 'companions', 'pools', 'initiative',
279
- 'karma', 'nuyen', 'edge', 'grid',
300
+ 'karma', 'nuyen', 'edge', 'grid', 'bodies',
280
301
  ];
281
302
  /** Refusal reasons already written to the session log -- one line each. */
282
303
  const announcedRefusals = new Map();
@@ -1188,6 +1188,29 @@ export class SharedRunSession {
1188
1188
  }
1189
1189
  if (!verb)
1190
1190
  return '';
1191
+ // PULLING SOMEBODY INTO A LIVE RUN (user 2026-09-11: "ensure that I
1192
+ // can pull in another runner during a live shared run, and the run
1193
+ // won't collapse"). The crew ledger is hub-only (crew.ts), and the
1194
+ // session's party was frozen at createSession -- so out here "crew
1195
+ // invite" had nowhere to go. The server adds the seat to the OPEN
1196
+ // session (sideQuest.inviteToOpen); their commlink buzzes through
1197
+ // the same invite feed, and their taxi seats them through the same
1198
+ // addPlayer a late joiner always used. Nothing about the scene is
1199
+ // touched: the job was sized at draft time and stays that size.
1200
+ const inviteMatch = /^(?:crew|party)\s+invite\s+(.+)$/i.exec(input.trim());
1201
+ if (inviteMatch) {
1202
+ try {
1203
+ const res = await HubLink.call('sideQuest.inviteToOpen', {
1204
+ sessionID: this.sessionID,
1205
+ query: inviteMatch[1].trim(),
1206
+ });
1207
+ return `Invite sent to ${res.memberRunner} -- a seat at this table, mid-job.${res.online ? '' : ` (They're off the street -- it'll wait on their link.)`}`;
1208
+ }
1209
+ catch (err) {
1210
+ const e = err;
1211
+ return `{yellow-fg}${e.reason ?? e.message ?? 'The offer does not go through.'}{/yellow-fg}`;
1212
+ }
1213
+ }
1191
1214
  // THE RIDE HOME: on a shared run the taxi is the settlement
1192
1215
  // handshake, not an engine verb -- the leader closes the table for
1193
1216
  // everyone (snapshots persist, shares already on each stick); a
@@ -1296,6 +1319,18 @@ export class SharedRunSession {
1296
1319
  restorePlayer(g.player, cloud.player);
1297
1320
  if (g.saveFilePath)
1298
1321
  writeSaveAtomic(g.saveFilePath, cloud);
1322
+ // THE HOMECOMING OWED (8izFwokbehjHzkEAn, 3gFcHfheGSMPtpCFh): the
1323
+ // server settled the table and left the aftermath on the save;
1324
+ // this hub Game never went anywhere, so it pays it now -- rent,
1325
+ // the DocWagon year, the Johnson's opinion -- and the homecoming
1326
+ // save it writes clears the flag. After the disk write above, so
1327
+ // the settled state is what lands, not the cloud's pre-settled one.
1328
+ const owed = cloud.game?.pendingHomecoming;
1329
+ if (owed) {
1330
+ const lines = g.applyHostedHomecoming(owed);
1331
+ if (lines)
1332
+ Logger.getInstance().logWithColor(lines, 'green');
1333
+ }
1299
1334
  }
1300
1335
  }
1301
1336
  catch (err) {
@@ -1241,7 +1241,15 @@ export function lockedRouteHint(room, direction, door) {
1241
1241
  if (door.heldBy) {
1242
1242
  return ` It's barred from ${door.heldBy.room} -- ${door.heldBy.device} is over there, not here.`;
1243
1243
  }
1244
- // A plain keyed door has no device and no route to invent.
1244
+ // A PLAIN KEYED DOOR (S8WdW7gm9tFGaxeCi): there IS a route, and the
1245
+ // Door knows it -- the key item it was locked with, and the one verb
1246
+ // that works it. The comment here used to say "no route to invent";
1247
+ // naming a route that exists is not inventing one, and the silence
1248
+ // read as a soft-lock in play. Scene-factory step 4.95 guarantees the
1249
+ // named key is a real item somewhere in the scene.
1250
+ if (door.unlockItemOrSolution) {
1251
+ return ` It takes ${door.unlockItemOrSolution} -- "unlock ${String(direction).toLowerCase()}" with it on you.`;
1252
+ }
1245
1253
  return '';
1246
1254
  }
1247
1255
  export function doorwayKindOf(spotName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.162.0",
3
+ "version": "5.164.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",