@maka/maka-cli 5.145.0 → 5.147.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.145.0",
3
+ "version": "5.147.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.",
@@ -73,6 +73,14 @@ export class AdvanceCommand extends Command {
73
73
  this.actor.performAction('trains', skill);
74
74
  this.logger.write(`${actor.name} advanced ${skill} to ${next} (spent ${cost} karma, ${actor.karma} left).`);
75
75
  this.scene.updateStatus();
76
+ // A DURABLE CHANGE (fDswPCSf8tGhMPMZ6, same shape as vR8ajvm3wXdexxdF2:
77
+ // buy/sell learned this, advance never did). Karma spent and a skill
78
+ // raised is exactly the kind of change equip.ts already asks for a
79
+ // save over -- without it, a player who trains and quits before the
80
+ // next autosave beat reloads to a lower skill AND the karma still
81
+ // gone, and the web character sheet (reading the last saved doc)
82
+ // shows the stale rating.
83
+ this.game?.requestSave('advance');
76
84
  return `You drill ${skill} until it sticks: ${current} -> ${next}. (${cost} karma spent, ${actor.karma} left.)`;
77
85
  }
78
86
  trainAttribute(attr) {
@@ -111,6 +119,7 @@ export class AdvanceCommand extends Command {
111
119
  this.actor.performAction('trains', attr);
112
120
  this.logger.write(`${actor.name} advanced ${attr} to base ${attr === 'magic' ? actor.baseMagic : actor.baseResonance} (effective ${effective}; spent ${cost} karma, ${actor.karma} left).`);
113
121
  this.scene.updateStatus();
122
+ this.game?.requestSave('advance');
114
123
  return `The talent deepens: ${attr} ${base} -> ${base + 1}${actor.essenceBurn > 0 ? ` (effective ${effective} after the chrome's toll)` : ''}. (${cost} karma spent, ${actor.karma} left.)`;
115
124
  }
116
125
  const current = actor[attr];
@@ -136,6 +145,7 @@ export class AdvanceCommand extends Command {
136
145
  this.actor.performAction('trains', attr);
137
146
  this.logger.write(`${actor.name} advanced ${attr} to ${next} (spent ${cost} karma, ${actor.karma} left).`);
138
147
  this.scene.updateStatus();
148
+ this.game?.requestSave('advance');
139
149
  const trackNote = attr === 'body' ? ` Your physical track grows with it.` : attr === 'reaction' ? ` Your stun track grows with it.` : '';
140
150
  return `Weeks of drills compressed into downtime: ${attr} ${current} -> ${next}. (${cost} karma spent, ${actor.karma} left.)${trackNote}`;
141
151
  }
@@ -0,0 +1,54 @@
1
+ import { Command } from './command.js';
2
+ /**
3
+ * "there should be a command for counterspelling" (3vCH5uQvpKZdjzWYQ).
4
+ *
5
+ * There already is counterspelling -- it just isn't a command. RAW
6
+ * (p.294) is a STANDING PARRY: a Counterspelling-skilled defender's
7
+ * dice roll straight into the defense test the instant a hostile spell
8
+ * targets them, and a co-located party caster can extend theirs over a
9
+ * teammate the same way ("one source, no stacking" -- the better rating
10
+ * covers, they don't add). combat-exchange.ts already does exactly
11
+ * this, automatically, on every spell strike, and narrates it on the
12
+ * Defense line ("+N counterspell -- Name's cover") when it bites.
13
+ *
14
+ * So the report's actual gap is not mechanics, it's that typing
15
+ * "counterspell" got "I don't understand the command" -- a player
16
+ * reaching for a verb that (correctly, per RAW) does not exist. This
17
+ * command answers that reach with the truth instead of a parser error:
18
+ * what protection they're already carrying and why nothing needs to be
19
+ * typed to use it.
20
+ *
21
+ * ONE HONEST GAP: canon also lets a defender who has already spent
22
+ * their Free Action declare protection reactively as an Interrupt
23
+ * Action at Initiative -5 (p.294). This engine's Action Phase economy
24
+ * (utilities/action-cost.ts) has no off-phase Free Action today --
25
+ * `IBillOptions.anyPhase` is declared and reserved for exactly this,
26
+ * unused by anything yet -- so the reactive-declare half of spell
27
+ * defense isn't modeled. What's implemented (the standing parry, which
28
+ * is what actually answers "I have counterspelling, why didn't it
29
+ * help") works with no action spent at all, which is the more common
30
+ * case by far.
31
+ */
32
+ export class CounterspellCommand extends Command {
33
+ static verb = 'counterspell';
34
+ static description = 'Check your counterspelling cover. Nothing to spend: your Counterspelling skill (or a co-located party caster\'s, if theirs is better) automatically rolls into the defense the instant a hostile spell targets you or an ally.';
35
+ async execute(_args = []) {
36
+ const actor = this.actor;
37
+ if (actor.isAspected && !actor.canWorkSorcery()) {
38
+ return `Counterspelling is Sorcery, and your gift is aspected elsewhere -- you can see the weave, not turn it.`;
39
+ }
40
+ if (typeof actor.isCaster !== 'function' || !actor.isCaster()) {
41
+ return `Counterspelling takes the Art or a spell formula, grimoire, or focus on you -- you have none.`;
42
+ }
43
+ const rating = actor.skillRating('counterspelling');
44
+ if (rating <= 0) {
45
+ return `You have no Counterspelling trained -- "qualities" or "advance" at home if you want to pick it up.`;
46
+ }
47
+ return `Counterspelling isn't something you trigger -- it's a standing parry (SR5 p.294).`
48
+ + `\nEvery hostile spell that targets you, or a party member you're co-located with, automatically`
49
+ + `\nrolls your counterspelling ${rating} into their defense test -- no action spent. If more than one`
50
+ + `\ncaster in the room could cover the same target, the best single rating applies (it doesn't stack).`
51
+ + `\nWatch the Defense line for "+${rating} counterspell" when it bites.`;
52
+ }
53
+ }
54
+ //# sourceMappingURL=counterspell.js.map
@@ -162,6 +162,24 @@ export function buildReproScene(fixture) {
162
162
  throw new ReproFixtureError(`"${misplacedIce.name}" is ice, so it cannot also stand in a doorway, guard, circulate or carry a gun. `
163
163
  + `Ice lives in the host, not on the floor -- drop the meat-side flags, or drop "ice".`);
164
164
  }
165
+ // A SEED HAS ONE PLANE. astral maps to 'astral' the same way ice maps
166
+ // to 'matrix' (npc-seed.ts) -- asking for both is asking the builder
167
+ // to pick a lie.
168
+ const bothPlanes = npcs.find(n => n.ice && n.astral);
169
+ if (bothPlanes) {
170
+ throw new ReproFixtureError(`"${bothPlanes.name}" is asked to be both ice and astral -- an actor has one plane, pick one.`);
171
+ }
172
+ // AN ASTRAL FORM CANNOT HOLD A DOOR SHUT OR CARRY A GUN. "guarding"
173
+ // means blocking meat movement, and "an astral presence has no body
174
+ // to block with" IS the fix this flag exists to bench -- a fixture
175
+ // asking for both would prove the opposite of what it claims. armed
176
+ // hands out a pistol and a commlink, and an astral form carries no
177
+ // physical object (SR5 p.301-302, Spirit Basics).
178
+ const contradictoryAstral = npcs.find(n => n.astral && (n.guarding || n.armed));
179
+ if (contradictoryAstral) {
180
+ throw new ReproFixtureError(`"${contradictoryAstral.name}" is astral, so it cannot guard a door or carry a gun -- `
181
+ + `an astral form has no body to block or hold anything with. Drop the flag, or drop "astral".`);
182
+ }
165
183
  const barrier = fixture.barrier;
166
184
  const direction = barrier?.sealsExit ?? Direction.NORTH;
167
185
  const startSide = fixture.start ?? 'barrier';
@@ -339,6 +357,12 @@ export function buildReproScene(fixture) {
339
357
  };
340
358
  }
341
359
  return {
360
+ // UNLIKE ice, astral does not move the NPC anywhere -- it keeps
361
+ // the ordinary placement below. The whole point of the fix this
362
+ // flag benches is that an astral actor stands right where a meat
363
+ // one would and still does not block (seatingIn is unchanged by
364
+ // the fix; only WHETHER it blocks changed, never WHERE it sits).
365
+ ...(npc.astral ? { plane: 'astral' } : {}),
342
366
  description: npc.concept,
343
367
  dialogue: [],
344
368
  player: {
@@ -170,6 +170,7 @@ import { SneakCommand } from './commands/sneak.js';
170
170
  import { PersuadeCommand } from './commands/persuade.js';
171
171
  import { BluffCommand } from './commands/bluff.js';
172
172
  import { DispelCommand } from './commands/dispel.js';
173
+ import { CounterspellCommand } from './commands/counterspell.js';
173
174
  import { BreachCommand } from './commands/breach.js';
174
175
  import { EmoteCommand } from './commands/emote.js';
175
176
  import { OverwatchCommand } from './commands/overwatch.js';
@@ -2648,6 +2649,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
2648
2649
  CommandFactory.registerCommand('dispel', DispelCommand);
2649
2650
  CommandFactory.registerCommand('dispell', DispelCommand);
2650
2651
  CommandFactory.registerCommand('breach', BreachCommand);
2652
+ // "counterspell" is not an action to spend -- it explains the
2653
+ // standing parry combat-exchange.ts already applies automatically
2654
+ // (3vCH5uQvpKZdjzWYQ).
2655
+ CommandFactory.registerCommand('counterspell', CounterspellCommand);
2651
2656
  // Free-form roleplay ("me" is the classic MUD spelling).
2652
2657
  CommandFactory.registerCommand('emote', EmoteCommand);
2653
2658
  CommandFactory.registerCommand('me', EmoteCommand);
@@ -6051,7 +6056,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
6051
6056
  // "players" folded back under crew after playtesting ("crew
6052
6057
  // players" / "crew street"); the bare verb still answers quietly.
6053
6058
  { title: 'party', entries: [['crew', 'party'], ['hire'], ['dismiss'], ['train'], ['order'], ['lead'], ['command'], ['deploy'], ['recall'], ['stow']] },
6054
- { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense']] },
6059
+ { title: 'magic', entries: [['spells'], ['cast'], ['summon', 'conjure'], ['project', 'astral'], ['return'], ['assense'], ['counterspell']] },
6055
6060
  { title: 'matrix', entries: [['jack'], ['jackpoint'], ['deck'], ['load'], ['unload'], ['hack'], ['mark'], ['download'], ['enter'], ['exit-host'], ['hop'], ['tap', 'splice'], ['snoop'], ['overwatch', 'os'], ['pan'], ['ar'], ['aros'], ['silent'], ['reboot'], ['agent'], ['drone'], ['jump', 'rig']] },
6056
6061
  // The Emerged get their own shelf (player request: "there MUST be
6057
6062
  // resonance" -- matrix is the place, Resonance is the talent).
@@ -20,8 +20,11 @@
20
20
  * would themselves have been a MINOR bump had a version already
21
21
  * existed. Read it as "schema, second edition", not "second edition
22
22
  * broke something".
23
+ *
24
+ * 2.4.0 adds IReproNpc.astral -- purely additive, older drafts still
25
+ * valid.
23
26
  */
24
- export const REPRO_SCHEMA_VERSION = '2.3.0';
27
+ export const REPRO_SCHEMA_VERSION = '2.4.0';
25
28
  /** THE LIVE REASON, from the field or the card's own words.
26
29
  *
27
30
  * `repro.live` is authoritative -- but the server's validator must
@@ -70,9 +70,9 @@ import { UsageError } from '../../error.js';
70
70
  */
71
71
  const MCP_SERVER_INSTRUCTIONS = `This server exposes the game-backlog admin actions as tools. Every tool is a thin wrapper over the same REST routes the human web board and CLI already use -- role checks, evidence gates, and audit logging all apply exactly as they do for a human admin. Nothing here bypasses them.
72
72
 
73
- THE LOOP IS NEARLY AUTOMATED BY DESIGN -- that is the whole reason this server exists instead of a human doing this work by hand. For each item: read it in full (backlog_get), find the root cause in the relevant repo (maka-cli-src for the CLI/game engine, maka-cli.com for the site/backend -- see sources-1/CLAUDE.md for which repo owns what), fix it, write/run tests, build, lint, typecheck -- then commit, version-bump (maka-cli-src releases go through the full chain: push -> build -> publish -> GitHub release + site pin -> the pin auto-deploys the site; maka-cli.com-only changes need either the next pin or a manual "gh workflow run deploy.yml"), and once the fix is committed AND actually deployed live, call backlog_advance with the commit sha(s) to move the item to "ready-for-bench".
73
+ THE LOOP IS NEARLY AUTOMATED BY DESIGN -- that is the whole reason this server exists instead of a human doing this work by hand. For each item: read it in full (backlog_get), find the root cause in the relevant repo (maka-cli-src for the CLI/game engine, maka-cli.com for the site/backend -- see sources-1/CLAUDE.md for which repo owns what), fix it, write/run tests, build, lint, typecheck -- then commit, version-bump (maka-cli-src releases go through the full chain: push -> build -> publish -> GitHub release + site pin -> the pin auto-deploys the site; maka-cli.com-only changes need either the next pin or a manual "gh workflow run deploy.yml"), and once the fix is committed AND actually deployed live: backlog_advance(status: "ready-for-bench", commits: [sha]) -> backlog_draft_bench -> backlog_dry_run_bench (read the transcript it returns -- this is automated QA, not the reporter's confirmation, and needs no human) -> if the transcript demonstrates the fix, backlog_advance(status: "in-review", repro: <the same spec that just dry-ran clean>), which promotes the draft to a real bench AND notifies the reporter in one call. That is as far as automation goes: only the reporter can sign off or reject from here, in-game -- see "WHAT THIS SERVER DELIBERATELY DOES NOT EXPOSE" below for why that boundary is real and the bench-verification step is not.
74
74
 
75
- DO NOT PAUSE TO ASK PERMISSION for a routine step in that loop -- committing a clean, tested, single-purpose change, bumping a version, pushing, or letting the release chain run. That is exactly the friction this tooling was built to remove. Reserve a real stop for a production-only action with no easy rollback, or a failure that genuinely needs human judgment to interpret.
75
+ DO NOT PAUSE TO ASK PERMISSION for a routine step in that loop -- committing a clean, tested, single-purpose change, bumping a version, pushing, letting the release chain run, or dry-running a drafted bench. That is exactly the friction this tooling was built to remove. Reserve a real stop for a production-only action with no easy rollback, or a failure that genuinely needs human judgment to interpret.
76
76
 
77
77
  DESIGN DECISIONS THAT COME UP WHILE FIXING ARE NOT A STOP EITHER. Make the call, then record the decision and its reasoning as a backlog_comment (kind: "note") on the item itself -- that comment is the durable record a later reviewer or the reporter can read; a live question to the human is the thing this loop exists to avoid. Only escalate a design question directly to the human when the choice changes externally-visible behavior in a way nobody could reconstruct from the code and the note.
78
78
 
@@ -82,7 +82,7 @@ SWEEP THE FAMILY. If a root cause affects more than one open item (the same clam
82
82
 
83
83
  "ready-for-bench" MEANS COMMITTED *AND* DEPLOYED, not just committed -- a bench built against undeployed code would replay the old bug and report the fix as failed. Don't advance to ready-for-bench before confirming the deploy actually succeeded.
84
84
 
85
- WHAT THIS SERVER DELIBERATELY DOES NOT EXPOSE, and why: signoff/reject/close are the reporter's own verbs -- only the person who hit the bug can confirm it is actually gone in play, and an AI closing its own fix would be exactly the self-certification this project's design already rejects for human admins. grant/revoke/delete/top-priority are rarer, higher-blast-radius actions left to a human directly.
85
+ WHAT THIS SERVER DELIBERATELY DOES NOT EXPOSE, and why: signoff/reject/close are the reporter's own verbs -- only the person who hit the bug can confirm it is actually gone in play, and an AI closing its own fix would be exactly the self-certification this project's design already rejects for human admins. grant/revoke/delete/top-priority are rarer, higher-blast-radius actions left to a human directly. NOTE THE DIFFERENCE FROM backlog_dry_run_bench: proving a bench SCRIPT replays correctly is a technical fact with no subjective side, and it needs no human -- it is not the same gate as the reporter deciding their own bug is gone, and must never be treated as though it were (that confusion cost a full stop the first time this loop ran for real).
86
86
 
87
87
  A "failed" vetting state means the automated vetting pipeline itself errored (no lore-search hits, or the model call failed) -- it is NOT a judgment that the item lacks merit. Treat those items as needing your own diagnosis from scratch, same as any other open item.`;
88
88
  export function registerBacklogMcp(parent) {
@@ -177,6 +177,7 @@ export function registerBacklogMcp(parent) {
177
177
  const { z } = await import('zod');
178
178
  const { ensureServiceSession } = await import('./sideQuest/utilities/service-account-auth.js');
179
179
  const { fetchBacklog, fetchBacklogItemById, patchBacklogItem, draftBacklogBench, postBacklogComment, BACKLOG_STATUSES, } = await import('./sideQuest/utilities/backlog.js');
180
+ const { dryRunRepro } = await import('./sideQuest/utilities/repro-launch.js');
180
181
  const statusEnum = BACKLOG_STATUSES;
181
182
  /** Every tool call goes through this: reuse the cached session, and
182
183
  * on the first `expired` outcome, mint a fresh one and retry
@@ -236,29 +237,75 @@ export function registerBacklogMcp(parent) {
236
237
  }, async ({ itemId, text, kind, awaitingReporter }) => toolResult(await withSession(async (token) => ({
237
238
  outcome: await postBacklogComment(itemId, text, { kind, awaitingReporter }, token),
238
239
  }))));
240
+ // MIRRORS IRepro (types/repro.ts), LOOSELY. `steps` is the one field
241
+ // that actually matters to this tool and to `dryRunRepro` -- the
242
+ // server's own validateRepro is the real authority on the rest, so
243
+ // `fixture`'s rich nested shape (barrier/npcs/items/level/host) is
244
+ // passed through as an opaque object rather than re-declared here.
245
+ // Re-mirroring a shape this deep in a second place is exactly the
246
+ // kind of drift this codebase's own comments repeatedly warn about;
247
+ // letting the server validate it is the one copy that matters.
248
+ const reproSchema = z.object({
249
+ fixture: z.record(z.string(), z.unknown()).optional(),
250
+ steps: z.array(z.string()).min(1),
251
+ premise: z.string().optional(),
252
+ expect: z.string().optional(),
253
+ minVersion: z.string().optional(),
254
+ live: z.string().optional(),
255
+ });
239
256
  server.registerTool('backlog_advance', {
240
257
  title: 'Advance a backlog item (attach evidence / change status)',
241
- description: `Move a backlog item's status and/or attach fix-commit evidence. This is the
242
- actual resolution action -- it will be refused by the server (with a message explaining
243
- why) if you try to enter "in-review" or "complete" without at least one commit attached,
244
- or "complete" without the reporter's sign-off already on the item. This tool never signs
245
- off or rejects a fix on the reporter's behalf -- only the person who filed the item can
246
- do that, in-game.`,
258
+ description: `Move a backlog item's status, attach fix-commit evidence, and/or promote a
259
+ dry-run-verified bench (see backlog_dry_run_bench) from reproDraft to a real repro. This
260
+ is the actual resolution action -- it will be refused by the server (with a message
261
+ explaining why) if you try to enter "in-review" or "complete" without at least one commit
262
+ attached, or "complete" without the reporter's sign-off already on the item. This tool
263
+ never signs off or rejects a fix on the reporter's behalf -- only the person who filed the
264
+ item can do that, in-game.`,
247
265
  inputSchema: {
248
266
  itemId: z.string(),
249
267
  status: z.enum(statusEnum),
250
268
  commits: z.array(z.string()).optional()
251
269
  .describe('Fix-commit SHAs to attach (merged with whatever is already there).'),
270
+ repro: reproSchema.optional()
271
+ .describe('Promote a bench to real: pass the exact spec that just dry-ran clean via backlog_dry_run_bench. Clears any pending reproDraft.'),
252
272
  },
253
- }, async ({ itemId, status, commits }) => toolResult(await withSession((token) => patchBacklogItem(itemId, { status, commits }, token))));
273
+ }, async ({ itemId, status, commits, repro }) => toolResult(await withSession((token) => patchBacklogItem(itemId, { status, commits, repro }, token))));
254
274
  server.registerTool('backlog_draft_bench', {
255
275
  title: 'Draft an AI bench for a backlog item',
256
- description: 'Ask the server to AI-draft a two-room repro bench for one item (lands in reproDraft, not repro -- a human has to dry-run and promote it separately via backlog_advance with a repro body). Costs a model call server-side.',
276
+ description: 'Ask the server to AI-draft a two-room repro bench for one item (lands in reproDraft, not repro -- dry-run it with backlog_dry_run_bench, then promote it with backlog_advance\'s repro param). Costs a model call server-side.',
257
277
  inputSchema: {
258
278
  itemId: z.string(),
259
279
  verbs: z.array(z.string()).optional().describe('The verbs available to reference in the bench steps.'),
260
280
  },
261
281
  }, async ({ itemId, verbs }) => toolResult(await withSession((token) => draftBacklogBench(itemId, token, verbs))));
282
+ server.registerTool('backlog_dry_run_bench', {
283
+ title: 'Dry-run a backlog item\'s bench, headlessly',
284
+ description: `Replay a backlog item's bench steps against a real boot of the game engine
285
+ -- no terminal, no login, no human -- and return the transcript (including dice/mechanics
286
+ lines). Proves the drafted bench (or an already-attached one) reproduces the reported
287
+ behavior BEFORE it is promoted or shown to the reporter. Prefers the item's pending
288
+ reproDraft; falls back to its attached repro if there is no draft. This is technical
289
+ verification, not the reporter's in-play confirmation -- reading the transcript and
290
+ judging whether it demonstrates the fix is exactly the kind of judgment call this tool
291
+ exists to let you make yourself.`,
292
+ inputSchema: { itemId: z.string() },
293
+ }, async ({ itemId }) => {
294
+ const result = await withSession((token) => fetchBacklogItemById(itemId, token));
295
+ if (result.outcome !== 'ok' || !result.item)
296
+ return toolResult(result);
297
+ const spec = result.item.reproDraft?.spec ?? result.item.repro;
298
+ if (!spec || !Array.isArray(spec.steps) || spec.steps.length === 0) {
299
+ return toolResult({ outcome: 'rejected', message: 'No reproDraft or repro on this item yet -- call backlog_draft_bench first.' });
300
+ }
301
+ try {
302
+ const { lines, warnings } = await dryRunRepro(spec);
303
+ return toolResult({ outcome: 'ok', transcript: lines.join('\n'), warnings });
304
+ }
305
+ catch (e) {
306
+ return toolResult({ outcome: 'rejected', message: `Dry run failed to boot: ${e.message}` });
307
+ }
308
+ });
262
309
  const transport = new StdioServerTransport();
263
310
  await server.connect(transport);
264
311
  console.error('[play:backlog:mcp] ready');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.145.0",
3
+ "version": "5.147.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.",