@maka/maka-cli 5.61.0 → 5.62.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 (109) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +4 -1
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.d.ts +31 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +95 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js.map +1 -0
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js +4 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/backlog.js.map +1 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +9 -0
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +62 -23
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js +5 -5
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/catalog.js.map +1 -1
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js +41 -2
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/contacts.js.map +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.d.ts +7 -14
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +86 -51
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js.map +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/deck.js +1 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/deck.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js +4 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +6 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/follow.d.ts +37 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/follow.js +216 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/follow.js.map +1 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +14 -3
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/install.js +4 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/install.js.map +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +6 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/jobs.js +3 -3
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/jobs.js.map +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js +1 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/look.d.ts +21 -1
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +165 -59
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +5 -2
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -1
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/note.js +9 -3
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/note.js.map +1 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/pan.js.map +1 -1
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/players.js +4 -1
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/players.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +4 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js.map +1 -1
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/roles.js +4 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/roles.js.map +1 -1
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/say.d.ts +21 -0
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +95 -8
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +3 -3
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js +4 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/silent.js.map +1 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/sin.js +4 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/sin.js.map +1 -1
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js.map +1 -1
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +5 -1
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js.map +1 -1
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +21 -0
  69. package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +4 -1
  71. package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js.map +1 -1
  72. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +13 -1
  73. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +78 -4
  74. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +43 -1
  76. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +9 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/game.js +77 -18
  79. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  80. package/bundle/typescript/src/commands/game/sideQuest/headless.js +58 -0
  81. package/bundle/typescript/src/commands/game/sideQuest/headless.js.map +1 -1
  82. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +8 -0
  83. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +129 -59
  84. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  85. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +4 -1
  86. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +14 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  88. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +1 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  90. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +6 -0
  91. package/bundle/typescript/src/commands/game/sideQuest/ui.js +21 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
  93. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.d.ts +39 -0
  94. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +111 -0
  95. package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js.map +1 -0
  96. package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.d.ts +76 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +98 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js.map +1 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/utilities/generation-capture.js +21 -9
  100. package/bundle/typescript/src/commands/game/sideQuest/utilities/generation-capture.js.map +1 -1
  101. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.d.ts +16 -0
  102. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +27 -0
  103. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js.map +1 -1
  104. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +3 -1
  105. package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js.map +1 -1
  106. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.d.ts +26 -2
  107. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +92 -7
  108. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js.map +1 -1
  109. package/package.json +1 -1
@@ -72,12 +72,14 @@ import { PickCommand } from './commands/pick.js';
72
72
  import { HackCommand } from './commands/hack.js';
73
73
  import { CastCommand } from './commands/cast.js';
74
74
  import { StanceCommand } from './commands/stance.js';
75
+ import { FollowCommand, UnfollowCommand } from './commands/follow.js';
75
76
  import { SurrenderCommand } from './commands/surrender.js';
76
77
  import { JackCommand } from './commands/jack.js';
77
78
  import { ProjectCommand, ReturnCommand } from './commands/project.js';
78
79
  import { AssenseCommand } from './commands/assense.js';
79
80
  import { DroneCommand } from './commands/drone.js';
80
81
  import { PanCommand } from './commands/pan.js';
82
+ import { ArCommand, ArosCommand } from './commands/ar.js';
81
83
  import { RebootCommand } from './commands/reboot.js';
82
84
  import { BrandishCommand, HolsterCommand } from './commands/brandish.js';
83
85
  import { AdvanceCommand } from './commands/advance.js';
@@ -198,7 +200,16 @@ export default class Game {
198
200
  /** Verbs that must run on the player's OWN machine even mid-shared-run:
199
201
  * they write local files or read local state, and mean nothing to the
200
202
  * table. See the guard in handleInput. */
201
- static LOCAL_ONLY_VERBS = new Set(['note', 'backlog', 'roles']);
203
+ static LOCAL_ONLY_VERBS = new Set(['note', 'backlog', 'roles', 'hints']);
204
+ /** The same rule at PHRASE level, for a local-only setting that hangs
205
+ * off a verb which must stay remote. "map" reads the live scene and
206
+ * belongs to the server in a shared run; "map overlay" only flips a
207
+ * preference in ~/.meteor/.maka/global-config.json and belongs here.
208
+ * Found 2026-08-27 -- this and `hints` were the last two instances of
209
+ * the `note` bug fixed in v5.57.0: mid-shared-run they crossed the
210
+ * wire, wrote the SERVER's config file, and the player's own toggle
211
+ * silently did nothing. */
212
+ static LOCAL_ONLY_PHRASES = new Set(['map overlay']);
202
213
  /** An account-less runner: nothing about them leaves this machine.
203
214
  * Read by anything that would otherwise push to maka-cli.com -- the
204
215
  * backlog command among them. */
@@ -939,7 +950,7 @@ export default class Game {
939
950
  if (wc)
940
951
  wc.payout = 0;
941
952
  const stained = this.player.addNotoriety(`killed their own Johnson on "${this.currentJobName ?? 'a job'}"`);
942
- Logger.getInstance().logWithColor(`\n=== JOB DEAD === You just killed your paymaster. Whatever this was worth, nobody's left to pay it${stained ? ` -- and word WILL travel (+1 Notoriety, now ${this.player.notoriety})` : ''}.`, 'red');
953
+ Logger.getInstance().logWithColor(`\nJOB DEAD -- You just killed your paymaster. Whatever this was worth, nobody's left to pay it${stained ? ` -- and word WILL travel (+1 Notoriety, now ${this.player.notoriety})` : ''}.`, 'red');
943
954
  }
944
955
  const entry = this.companions.find(c => c.npc.name === name);
945
956
  if (!entry)
@@ -1640,6 +1651,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1640
1651
  registerCommands() {
1641
1652
  CommandFactory.registerCommand('look', LookCommand);
1642
1653
  CommandFactory.registerCommand('go', GoCommand);
1654
+ // TAILING A PARTNER (commands/follow.ts): a human partner heels
1655
+ // only because they said so -- two players asked for this.
1656
+ CommandFactory.registerCommand('follow', FollowCommand);
1657
+ CommandFactory.registerCommand('unfollow', UnfollowCommand);
1643
1658
  CommandFactory.registerCommand('take', TakeCommand);
1644
1659
  CommandFactory.registerCommand('loot', LootCommand);
1645
1660
  CommandFactory.registerCommand('note', NoteCommand);
@@ -1722,6 +1737,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
1722
1737
  CommandFactory.registerCommand('assense', AssenseCommand);
1723
1738
  CommandFactory.registerCommand('drone', DroneCommand);
1724
1739
  CommandFactory.registerCommand('pan', PanCommand);
1740
+ // THE AR LAYER (commands/ar.ts): `ar` mutes your own overlay,
1741
+ // `aros` reads what it is showing you -- player requests
1742
+ // 2026-08-26, "we need AROs on anything online" and "there
1743
+ // should be a way to toggle AR".
1744
+ CommandFactory.registerCommand('ar', ArCommand);
1745
+ CommandFactory.registerCommand('aros', ArosCommand);
1725
1746
  CommandFactory.registerCommand('reboot', RebootCommand);
1726
1747
  CommandFactory.registerCommand('brandish', BrandishCommand);
1727
1748
  CommandFactory.registerCommand('draw', BrandishCommand);
@@ -2163,6 +2184,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2163
2184
  this._pendingRunSeed = save.game.pendingRun.seed;
2164
2185
  this._pendingRunTier = save.game.pendingRun.tier;
2165
2186
  this._pendingRide = save.game.pendingRun.ride;
2187
+ // A seed Krow DRAFTED is not a fare (see call.ts's taxi gate --
2188
+ // playtest 2026-08-27: "I was able to call a taxi before calling
2189
+ // Mr. Krow"). Saves written before this field carried no answer
2190
+ // and restore as UN-accepted: one extra "call Krow" once, rather
2191
+ // than reopening the hole for every existing campaign.
2192
+ this.pendingRunAccepted = save.game.pendingRun.accepted ?? false;
2166
2193
  }
2167
2194
  this.player.currentLocation = this._homeRoom;
2168
2195
  this.updateEquipment(this.player.equipment);
@@ -2256,7 +2283,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2256
2283
  gigAvailable: this.gigAvailable,
2257
2284
  activeGig: this.activeGig,
2258
2285
  pendingRun: this._pendingRunSeed && this._pendingRunTier !== undefined
2259
- ? { seed: this._pendingRunSeed, tier: this._pendingRunTier, ride: this._pendingRide }
2286
+ ? { seed: this._pendingRunSeed, tier: this._pendingRunTier, ride: this._pendingRide, accepted: this.pendingRunAccepted }
2260
2287
  : undefined,
2261
2288
  },
2262
2289
  hub: captureHubOverlay(this._hubScene, this._hubSeed, this.player, this._homeRoom),
@@ -2512,7 +2539,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2512
2539
  const fateLine = wrappedRun
2513
2540
  ? `The run's wrapped and the street swallows you back up.`
2514
2541
  : `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
2515
- 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')}` : ''}`;
2542
+ 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')}` : ''}`;
2516
2543
  }
2517
2544
  /**
2518
2545
  * KNIGHT ERRANT CHECKPOINT (SINs & getting caught, ruling 2026-08-24;
@@ -3429,8 +3456,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3429
3456
  if (player.plane === 'meat' && weaponItem && !player.weaponDrawn) {
3430
3457
  return `{white-fg}Holstered: ${weaponItem.name}${ammoNote}${hint(' -- "brandish"')}{/white-fg}`;
3431
3458
  }
3459
+ // White, not gray: gray is ANSI bright-black and illegible on
3460
+ // stock dark palettes -- the same ruling that moved the HUD
3461
+ // stance and the Tab ticker off it. These are the HUD's QUIET
3462
+ // states, not its hidden ones; they still have to be readable.
3432
3463
  if (weapon.name === 'bare fists')
3433
- return `{gray-fg}Unarmed{/gray-fg}`;
3464
+ return `{white-fg}Unarmed{/white-fg}`;
3434
3465
  return `Armed: ${weapon.name} (DV ${weapon.damage}${weapon.poolBonus > 0 ? `, +${weapon.poolBonus} dice` : ''})${ammoNote}`;
3435
3466
  }
3436
3467
  /**
@@ -3504,12 +3535,19 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3504
3535
  const armorNote = `Armor +${player.getArmorValue()}`;
3505
3536
  const hasLink = player.equipment.head?.commlink != null;
3506
3537
  const link = !hasLink
3507
- ? `{gray-fg}Link offline{/gray-fg}`
3538
+ ? `{white-fg}Link offline{/white-fg}`
3508
3539
  : player.runningSilent
3509
3540
  ? `{magenta-fg}Link SILENT{/magenta-fg}`
3510
3541
  : player.arOffline
3511
3542
  ? `{red-fg}AR CUT -- reboot{/red-fg}`
3512
- : `{${CALL_COLOR}-fg}Link online{/${CALL_COLOR}-fg}`;
3543
+ // MUTED BY CHOICE ranks LAST of the four on purpose: the two
3544
+ // states that COST you something -- silence you chose for a
3545
+ // price, and an overlay a hacker took -- outrank the one you
3546
+ // simply switched off. It reads "Link online" as well as "AR
3547
+ // OFF" because that IS the distinction the state exists to draw.
3548
+ : player.arMuted
3549
+ ? `{white-fg}Link online, AR OFF{/white-fg}`
3550
+ : `{${CALL_COLOR}-fg}Link online{/${CALL_COLOR}-fg}`;
3513
3551
  lines.push(`${armorNote} | ${link}`);
3514
3552
  // Lines 4+: transient states, present only while active. Future
3515
3553
  // contexts (astral projection, VR hot-sim) join here the same way.
@@ -3718,8 +3756,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3718
3756
  // player's own margin scribbles about the game. They have no
3719
3757
  // business crossing the wire, and the table has no business
3720
3758
  // hearing them. Run them on the machine that typed them.
3721
- const verb = input.trim().split(/\s+/)[0]?.toLowerCase() ?? '';
3722
- if (!Game.LOCAL_ONLY_VERBS.has(verb)) {
3759
+ const words = input.trim().toLowerCase().split(/\s+/);
3760
+ const verb = words[0] ?? '';
3761
+ const phrase = words.slice(0, 2).join(' ');
3762
+ if (!Game.LOCAL_ONLY_VERBS.has(verb) && !Game.LOCAL_ONLY_PHRASES.has(phrase)) {
3723
3763
  return this.remoteSession.command(input);
3724
3764
  }
3725
3765
  }
@@ -3853,7 +3893,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3853
3893
  * future command can't silently fall out of help.
3854
3894
  */
3855
3895
  static HELP_CATEGORIES = [
3856
- { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['map', 'exits'], ['search'], ['sneak']] },
3896
+ { title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach', 'run', 'sprint'], ['follow', 'unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
3857
3897
  { title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['drop'], ['give'], ['store'], ['equip'], ['unequip'], ['brandish', 'draw'], ['holster', 'stow'], ['reload'], ['use'], ['read']] },
3858
3898
  { title: 'combat', entries: [['attack', 'kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['stance'], ['surrender'], ['edge'], ['rest']] },
3859
3899
  // The party layer: everyone who walks (or flies, or manifests) at
@@ -3862,7 +3902,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3862
3902
  // players" / "crew street"); the bare verb still answers quietly.
3863
3903
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall']] },
3864
3904
  { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project'], ['return'], ['assense']] },
3865
- { title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['snoop'], ['overwatch', 'os'], ['pan'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
3905
+ { title: 'matrix', entries: [['jack'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
3866
3906
  // The Emerged get their own shelf (player request: "there MUST be
3867
3907
  // resonance" -- matrix is the place, Resonance is the talent).
3868
3908
  { title: 'resonance', entries: [['compile', 'decompile'], ['thread'], ['register']] },
@@ -3902,7 +3942,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3902
3942
  const pickedCategory = fuzzyPickName(query, categoryNames);
3903
3943
  if (pickedCategory) {
3904
3944
  const category = Game.HELP_CATEGORIES.find(c => c.title === pickedCategory);
3905
- const lines = [`=== ${category.title.toUpperCase()} ===`];
3945
+ const lines = [`${category.title.toUpperCase()}`];
3906
3946
  for (const [primary, ...aliases] of category.entries) {
3907
3947
  lines.push(`${BULLET}${primary}${aliases.length > 0 ? ` (${aliases.join(', ')})` : ''}: ${this.describeVerb(primary)}`);
3908
3948
  }
@@ -3929,13 +3969,32 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3929
3969
  // A row's key: the word after the primary verb when usage
3930
3970
  // starts with it, else its first word.
3931
3971
  const subKey = (s) => {
3932
- const t = s.usage.split(/\\s+/);
3972
+ // /\s+/, not /\\s+/ (found 2026-08-27). The escaped
3973
+ // backslash matched a LITERAL "\s" and never split, so
3974
+ // subKey returned the whole usage string. It looked fine
3975
+ // only because fuzzyPickName rescued it at the substring
3976
+ // tier -- "hire" matches inside "crew hire <name>" -- which
3977
+ // is exactly the kind of accident that stops working the
3978
+ // first time two rows share a word.
3979
+ const t = s.usage.split(/\s+/);
3933
3980
  return (t[0] === primary ? t[1] ?? t[0] : t[0]).replace(/[<>|]/g, '');
3934
3981
  };
3935
- const pickedSub = fuzzyPickName(subQuery, subs.map(subKey));
3936
- const sub = pickedSub ? subs.find(s => subKey(s) === pickedSub) : undefined;
3937
- if (sub)
3938
- return `${sub.usage}\n ${sub.long ?? sub.short}`;
3982
+ // DEDUPE, or an exact match reads as AMBIGUOUS and
3983
+ // fuzzyPickName returns undefined. Two rows legitimately
3984
+ // share a key -- `crew hire` (the board) and `hire <name>`
3985
+ // (sign someone) are one sub-verb with and without an
3986
+ // argument -- so the moment the split above started working,
3987
+ // "help crew hire" broke outright.
3988
+ const pickedSub = fuzzyPickName(subQuery, [...new Set(subs.map(subKey))]);
3989
+ // ...and then render EVERY row sharing that key, not just the
3990
+ // first. A player asking "help crew hire" wants both halves:
3991
+ // what the bare form shows them, and what naming somebody
3992
+ // does. Answering with one and silently dropping the other is
3993
+ // how "crew hire" came to be asked about in the first place.
3994
+ const matches = pickedSub ? subs.filter(s => subKey(s) === pickedSub) : [];
3995
+ if (matches.length > 0) {
3996
+ return matches.map(s => `${s.usage}\n ${s.long ?? s.short}`).join('\n\n');
3997
+ }
3939
3998
  return `${title}: no subcommand matching "${subQuery}".\n${subs.map(s => ` ${s.usage.padEnd(24)} ${s.short}`).join('\n')}`;
3940
3999
  }
3941
4000
  const lines = [`${title}: ${this.describeVerb(primary).split('. ')[0]}.`];
@@ -3952,7 +4011,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
3952
4011
  // The overview: category lines with primaries only, chunked so no
3953
4012
  // line outgrows the Mechanics column (help renders there now --
3954
4013
  // blessed's mid-list wrap lost the left-column alignment).
3955
- const lines = ['=== COMMANDS ==='];
4014
+ const lines = ['COMMANDS'];
3956
4015
  const pad = Math.max(...Game.HELP_CATEGORIES.map(c => c.title.length));
3957
4016
  const pushWrapped = (title, verbs) => {
3958
4017
  const rows = [];