@forbocai/test-game 0.6.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.
@@ -0,0 +1,694 @@
1
+ // src/lib/coverage.ts
2
+ var REQUIRED_GROUPS = [
3
+ "status",
4
+ "npc_lifecycle",
5
+ "npc_process_chat",
6
+ "memory_list",
7
+ "memory_recall",
8
+ "memory_store",
9
+ "memory_clear",
10
+ "memory_export",
11
+ "bridge_rules",
12
+ "bridge_validate",
13
+ "bridge_preset",
14
+ "soul_export",
15
+ "soul_import",
16
+ "soul_list",
17
+ "soul_chat",
18
+ "ghost_lifecycle",
19
+ "cortex_init"
20
+ ];
21
+
22
+ // src/game.ts
23
+ import { createInterface } from "readline/promises";
24
+ import { stdin as input, stdout as output } from "process";
25
+
26
+ // src/features/autoplay/slices/harnessSlice.ts
27
+ import { createSlice } from "@reduxjs/toolkit";
28
+ var initialState = {
29
+ covered: {}
30
+ };
31
+ var harnessSlice = createSlice({
32
+ name: "harness",
33
+ initialState,
34
+ reducers: {
35
+ markCovered: (state, action) => {
36
+ state.covered[action.payload] = true;
37
+ },
38
+ resetCoverage: (state) => {
39
+ state.covered = {};
40
+ }
41
+ }
42
+ });
43
+ var harnessReducer = harnessSlice.reducer;
44
+ var harnessActions = harnessSlice.actions;
45
+ var selectMissingGroups = (covered) => REQUIRED_GROUPS.filter((g) => !covered[g]);
46
+
47
+ // src/features/mechanics/slices/bridgeSlice.ts
48
+ import { createSlice as createSlice2 } from "@reduxjs/toolkit";
49
+ var initialState2 = {
50
+ maxJumpForce: 500,
51
+ maxMoveDistance: 2,
52
+ activePreset: "default"
53
+ };
54
+ var bridgeSlice = createSlice2({
55
+ name: "bridge",
56
+ initialState: initialState2,
57
+ reducers: {
58
+ setBridgeRules: (state, action) => {
59
+ Object.assign(state, action.payload);
60
+ },
61
+ loadBridgePreset: (state, action) => {
62
+ state.activePreset = action.payload;
63
+ if (action.payload === "social") {
64
+ state.maxMoveDistance = 1;
65
+ }
66
+ if (action.payload === "default") {
67
+ state.maxMoveDistance = 2;
68
+ }
69
+ }
70
+ }
71
+ });
72
+ var bridgeReducer = bridgeSlice.reducer;
73
+ var bridgeActions = bridgeSlice.actions;
74
+ var validateJump = (rules, force) => {
75
+ if (force > rules.maxJumpForce) {
76
+ return { valid: false, reason: `Jump force ${force} exceeds ${rules.maxJumpForce}` };
77
+ }
78
+ return { valid: true };
79
+ };
80
+ var capMoveDistance = (rules, requestedDistance) => ({
81
+ allowedDistance: Math.min(requestedDistance, rules.maxMoveDistance),
82
+ capped: requestedDistance > rules.maxMoveDistance
83
+ });
84
+
85
+ // src/features/mechanics/slices/socialSlice.ts
86
+ import { createSlice as createSlice3 } from "@reduxjs/toolkit";
87
+ var initialState3 = {};
88
+ var socialSlice = createSlice3({
89
+ name: "social",
90
+ initialState: initialState3,
91
+ reducers: {
92
+ setDialogue: (state, action) => {
93
+ state.activeDialogue = action.payload;
94
+ },
95
+ setTradeOffer: (state, action) => {
96
+ state.activeTrade = action.payload;
97
+ },
98
+ clearSocialState: (state) => {
99
+ state.activeDialogue = void 0;
100
+ state.activeTrade = void 0;
101
+ }
102
+ }
103
+ });
104
+ var socialReducer = socialSlice.reducer;
105
+ var socialActions = socialSlice.actions;
106
+
107
+ // src/features/mechanics/slices/stealthSlice.ts
108
+ import { createSlice as createSlice4 } from "@reduxjs/toolkit";
109
+ var initialState4 = {
110
+ doorOpen: false,
111
+ alertLevel: 0
112
+ };
113
+ var stealthSlice = createSlice4({
114
+ name: "stealth",
115
+ initialState: initialState4,
116
+ reducers: {
117
+ setDoorOpen: (state, action) => {
118
+ state.doorOpen = action.payload;
119
+ },
120
+ bumpAlert: (state, action) => {
121
+ state.alertLevel = Math.max(0, Math.min(100, state.alertLevel + action.payload));
122
+ }
123
+ }
124
+ });
125
+ var stealthReducer = stealthSlice.reducer;
126
+ var stealthActions = stealthSlice.actions;
127
+
128
+ // src/features/entities/slices/npcsSlice.ts
129
+ import { createEntityAdapter, createSlice as createSlice5 } from "@reduxjs/toolkit";
130
+ var npcsAdapter = createEntityAdapter();
131
+ var npcsSlice = createSlice5({
132
+ name: "npcs",
133
+ initialState: npcsAdapter.getInitialState(),
134
+ reducers: {
135
+ upsertNPC: npcsAdapter.upsertOne,
136
+ moveNPC: (state, action) => {
137
+ const target = state.entities[action.payload.id];
138
+ if (!target) return;
139
+ target.position = action.payload.position;
140
+ },
141
+ patchNPC: (state, action) => {
142
+ const target = state.entities[action.payload.id];
143
+ if (!target) return;
144
+ Object.assign(target, action.payload.patch);
145
+ }
146
+ }
147
+ });
148
+ var npcsReducer = npcsSlice.reducer;
149
+ var npcsActions = npcsSlice.actions;
150
+ var npcsSelectors = npcsAdapter.getSelectors(
151
+ (root) => root.npcs
152
+ );
153
+
154
+ // src/features/entities/slices/playerSlice.ts
155
+ import { createSlice as createSlice6 } from "@reduxjs/toolkit";
156
+ var initialState5 = {
157
+ name: "Scout",
158
+ hp: 100,
159
+ hidden: true,
160
+ position: { x: 1, y: 1 },
161
+ inventory: ["coin-pouch"]
162
+ };
163
+ var playerSlice = createSlice6({
164
+ name: "player",
165
+ initialState: initialState5,
166
+ reducers: {
167
+ setPosition: (state, action) => {
168
+ state.position = action.payload;
169
+ },
170
+ setHidden: (state, action) => {
171
+ state.hidden = action.payload;
172
+ },
173
+ patchPlayer: (state, action) => {
174
+ Object.assign(state, action.payload);
175
+ }
176
+ }
177
+ });
178
+ var playerReducer = playerSlice.reducer;
179
+ var playerActions = playerSlice.actions;
180
+
181
+ // src/features/store/slices/memorySlice.ts
182
+ import { createSlice as createSlice7 } from "@reduxjs/toolkit";
183
+ var initialState6 = {
184
+ records: []
185
+ };
186
+ var memorySlice = createSlice7({
187
+ name: "memory",
188
+ initialState: initialState6,
189
+ reducers: {
190
+ storeMemory: (state, action) => {
191
+ state.records.push(action.payload);
192
+ },
193
+ clearMemoryForNpc: (state, action) => {
194
+ state.records = state.records.filter((r) => r.npcId !== action.payload);
195
+ }
196
+ }
197
+ });
198
+ var memoryReducer = memorySlice.reducer;
199
+ var memoryActions = memorySlice.actions;
200
+
201
+ // src/features/store/slices/soulSlice.ts
202
+ import { createSlice as createSlice8 } from "@reduxjs/toolkit";
203
+ var initialState7 = {
204
+ exportsByNpc: {},
205
+ importedSoulTxIds: []
206
+ };
207
+ var soulSlice = createSlice8({
208
+ name: "soul",
209
+ initialState: initialState7,
210
+ reducers: {
211
+ markSoulExported: (state, action) => {
212
+ state.exportsByNpc[action.payload.npcId] = action.payload.txId;
213
+ },
214
+ markSoulImported: (state, action) => {
215
+ state.importedSoulTxIds.push(action.payload);
216
+ }
217
+ }
218
+ });
219
+ var soulReducer = soulSlice.reducer;
220
+ var soulActions = soulSlice.actions;
221
+
222
+ // src/features/terminal/slices/transcriptSlice.ts
223
+ import { createSlice as createSlice9 } from "@reduxjs/toolkit";
224
+ var initialState8 = {
225
+ entries: []
226
+ };
227
+ var transcriptSlice = createSlice9({
228
+ name: "transcript",
229
+ initialState: initialState8,
230
+ reducers: {
231
+ recordTranscript: (state, action) => {
232
+ state.entries.push(action.payload);
233
+ },
234
+ resetTranscript: (state) => {
235
+ state.entries = [];
236
+ }
237
+ }
238
+ });
239
+ var transcriptReducer = transcriptSlice.reducer;
240
+ var transcriptActions = transcriptSlice.actions;
241
+
242
+ // src/features/terminal/slices/uiSlice.ts
243
+ import { createSlice as createSlice10 } from "@reduxjs/toolkit";
244
+ var initialState9 = {
245
+ mode: "autoplay",
246
+ messages: []
247
+ };
248
+ var uiSlice = createSlice10({
249
+ name: "ui",
250
+ initialState: initialState9,
251
+ reducers: {
252
+ setMode: (state, action) => {
253
+ state.mode = action.payload;
254
+ },
255
+ addMessage: (state, action) => {
256
+ state.messages.push(action.payload);
257
+ }
258
+ }
259
+ });
260
+ var uiReducer = uiSlice.reducer;
261
+ var uiActions = uiSlice.actions;
262
+
263
+ // src/lib/commandRunner.ts
264
+ import { exec } from "child_process";
265
+ import { promisify } from "util";
266
+ var execAsync = promisify(exec);
267
+ var runCommand = async (command, mode) => {
268
+ if (mode === "simulate") {
269
+ return { status: "simulated", output: "Simulated command execution." };
270
+ }
271
+ try {
272
+ const { stdout, stderr } = await execAsync(command.command, { timeout: 1e4 });
273
+ const output2 = [stdout, stderr].filter(Boolean).join("\n").trim();
274
+ return { status: "ok", output: output2 || "Command completed." };
275
+ } catch (error) {
276
+ const output2 = [error?.stdout, error?.stderr, error?.message].filter(Boolean).join("\n").trim();
277
+ return { status: "error", output: output2 || "Command failed." };
278
+ }
279
+ };
280
+
281
+ // src/lib/render.ts
282
+ var cellAt = (pos, state) => {
283
+ const isBlocked = state.grid.blocked.some((b) => b.x === pos.x && b.y === pos.y);
284
+ if (isBlocked) return "#";
285
+ if (state.player.position.x === pos.x && state.player.position.y === pos.y) {
286
+ return "P";
287
+ }
288
+ for (const npc of Object.values(state.npcs.entities)) {
289
+ if (!npc) continue;
290
+ if (npc.position.x === pos.x && npc.position.y === pos.y) return npc.id === "miller" ? "M" : "D";
291
+ }
292
+ return ".";
293
+ };
294
+ var renderGrid = (state) => {
295
+ const rows = [];
296
+ for (let y = 0; y < state.grid.height; y += 1) {
297
+ const cells = [];
298
+ for (let x = 0; x < state.grid.width; x += 1) {
299
+ cells.push(cellAt({ x, y }, state));
300
+ }
301
+ rows.push(cells.join(" "));
302
+ }
303
+ return rows.join("\n");
304
+ };
305
+
306
+ // src/store.ts
307
+ import { configureStore } from "@reduxjs/toolkit";
308
+
309
+ // src/features/mechanics/slices/gridSlice.ts
310
+ import { createSlice as createSlice11 } from "@reduxjs/toolkit";
311
+ var initialState10 = {
312
+ width: 8,
313
+ height: 8,
314
+ blocked: [{ x: 4, y: 4 }, { x: 4, y: 5 }, { x: 6, y: 2 }]
315
+ };
316
+ var gridSlice = createSlice11({
317
+ name: "grid",
318
+ initialState: initialState10,
319
+ reducers: {
320
+ setGridSize: (state, action) => {
321
+ state.width = action.payload.width;
322
+ state.height = action.payload.height;
323
+ },
324
+ setBlocked: (state, action) => {
325
+ state.blocked = action.payload;
326
+ }
327
+ }
328
+ });
329
+ var gridReducer = gridSlice.reducer;
330
+ var gridActions = gridSlice.actions;
331
+
332
+ // src/features/store/slices/inventorySlice.ts
333
+ import { createSlice as createSlice12 } from "@reduxjs/toolkit";
334
+ var initialState11 = {
335
+ byOwner: {
336
+ player: ["coin-pouch"],
337
+ miller: ["medkit"]
338
+ }
339
+ };
340
+ var inventorySlice = createSlice12({
341
+ name: "inventory",
342
+ initialState: initialState11,
343
+ reducers: {
344
+ setOwnerInventory: (state, action) => {
345
+ state.byOwner[action.payload.ownerId] = action.payload.items;
346
+ }
347
+ }
348
+ });
349
+ var inventoryReducer = inventorySlice.reducer;
350
+ var inventoryActions = inventorySlice.actions;
351
+
352
+ // src/features/autoplay/slices/scenarioSlice.ts
353
+ import { createSlice as createSlice13 } from "@reduxjs/toolkit";
354
+ var steps = [
355
+ {
356
+ id: "stealth-door-open",
357
+ title: "Spatial Strategy & Stealth",
358
+ description: "Armory door is left open. Doomguard patrol processes observation and stores memory.",
359
+ eventType: "stealth",
360
+ commands: [
361
+ { group: "status", command: "forbocai status", expectedRoutes: ["GET /status"] },
362
+ {
363
+ group: "npc_lifecycle",
364
+ command: "forbocai npc create doomguard",
365
+ expectedRoutes: ["local only"]
366
+ },
367
+ {
368
+ group: "npc_lifecycle",
369
+ command: "forbocai npc state doomguard",
370
+ expectedRoutes: ["local only"]
371
+ },
372
+ {
373
+ group: "npc_lifecycle",
374
+ command: "forbocai npc update doomguard faction Doomguards",
375
+ expectedRoutes: ["local only"]
376
+ },
377
+ {
378
+ group: "npc_process_chat",
379
+ command: 'forbocai npc process doomguard "The armory door is open"',
380
+ expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
381
+ },
382
+ {
383
+ group: "memory_store",
384
+ command: 'forbocai memory store doomguard "Armory door found open at x:5, y:12"',
385
+ expectedRoutes: ["POST /npcs/{id}/memory"]
386
+ },
387
+ {
388
+ group: "memory_list",
389
+ command: "forbocai memory list doomguard",
390
+ expectedRoutes: ["GET /npcs/{id}/memory"]
391
+ },
392
+ {
393
+ group: "bridge_rules",
394
+ command: "forbocai bridge rules",
395
+ expectedRoutes: ["GET /bridge/rules"]
396
+ }
397
+ ]
398
+ },
399
+ {
400
+ id: "social-miller-encounter",
401
+ title: "Social Simulation",
402
+ description: "Miller encounter triggers recall, dialogue, and trade offer.",
403
+ eventType: "social",
404
+ commands: [
405
+ {
406
+ group: "npc_lifecycle",
407
+ command: "forbocai npc create miller",
408
+ expectedRoutes: ["local only"]
409
+ },
410
+ {
411
+ group: "npc_process_chat",
412
+ command: 'forbocai npc chat miller --text "I come in peace"',
413
+ expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
414
+ },
415
+ {
416
+ group: "memory_recall",
417
+ command: 'forbocai memory recall miller "player interaction"',
418
+ expectedRoutes: ["POST /npcs/{id}/memory/recall"]
419
+ },
420
+ {
421
+ group: "bridge_preset",
422
+ command: "forbocai bridge preset social",
423
+ expectedRoutes: ["POST /rules/presets/{id}"]
424
+ },
425
+ {
426
+ group: "soul_export",
427
+ command: "forbocai soul export miller",
428
+ expectedRoutes: ["POST /npcs/{id}/soul/export"]
429
+ },
430
+ {
431
+ group: "soul_list",
432
+ command: "forbocai soul list",
433
+ expectedRoutes: ["GET /souls"]
434
+ }
435
+ ]
436
+ },
437
+ {
438
+ id: "escape-realtime-pursuit",
439
+ title: "Real-Time Escape",
440
+ description: "Harness loop fires process commands and validates jump force via bridge rules.",
441
+ eventType: "escape",
442
+ commands: [
443
+ {
444
+ group: "bridge_validate",
445
+ command: "forbocai bridge validate doomguard-jump",
446
+ expectedRoutes: ["POST /bridge/validate", "POST /bridge/validate/{id}"]
447
+ },
448
+ {
449
+ group: "npc_process_chat",
450
+ command: 'forbocai npc process doomguard "Player is escaping"',
451
+ expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
452
+ },
453
+ {
454
+ group: "npc_process_chat",
455
+ command: 'forbocai npc process doomguard "Player jumped the gap"',
456
+ expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
457
+ },
458
+ {
459
+ group: "npc_process_chat",
460
+ command: 'forbocai npc process doomguard "Player reached the gate"',
461
+ expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
462
+ },
463
+ {
464
+ group: "ghost_lifecycle",
465
+ command: "forbocai ghost run smoke",
466
+ expectedRoutes: ["POST /ghost/run"]
467
+ },
468
+ {
469
+ group: "ghost_lifecycle",
470
+ command: "forbocai ghost status ghost-001",
471
+ expectedRoutes: ["GET /ghost/{id}/status"]
472
+ },
473
+ {
474
+ group: "ghost_lifecycle",
475
+ command: "forbocai ghost results ghost-001",
476
+ expectedRoutes: ["GET /ghost/{id}/results"]
477
+ },
478
+ {
479
+ group: "ghost_lifecycle",
480
+ command: "forbocai ghost stop ghost-001",
481
+ expectedRoutes: ["POST /ghost/{id}/stop"]
482
+ },
483
+ {
484
+ group: "ghost_lifecycle",
485
+ command: "forbocai ghost history",
486
+ expectedRoutes: ["GET /ghost/history"]
487
+ }
488
+ ]
489
+ },
490
+ {
491
+ id: "persistence-recovery",
492
+ title: "Persistence & Recovery",
493
+ description: "Exercises memory export/clear and soul import/chat continuity.",
494
+ eventType: "persistence",
495
+ commands: [
496
+ {
497
+ group: "memory_export",
498
+ command: "forbocai memory export doomguard",
499
+ expectedRoutes: ["local only"]
500
+ },
501
+ {
502
+ group: "memory_clear",
503
+ command: "forbocai memory clear doomguard",
504
+ expectedRoutes: ["DELETE /npcs/{id}/memory/clear"]
505
+ },
506
+ {
507
+ group: "soul_import",
508
+ command: "forbocai soul import tx-demo-001",
509
+ expectedRoutes: ["GET /souls/{txId}", "POST /npcs/import"]
510
+ },
511
+ {
512
+ group: "soul_chat",
513
+ command: 'forbocai soul chat doomguard --text "What do you remember?"',
514
+ expectedRoutes: ["POST /npcs/{id}/directive", "POST /npcs/{id}/context", "POST /npcs/{id}/verdict"]
515
+ },
516
+ {
517
+ group: "cortex_init",
518
+ command: "forbocai cortex init --remote",
519
+ expectedRoutes: ["POST /cortex/init"]
520
+ }
521
+ ]
522
+ }
523
+ ];
524
+ var scenarioSlice = createSlice13({
525
+ name: "scenario",
526
+ initialState: {
527
+ steps
528
+ },
529
+ reducers: {}
530
+ });
531
+ var scenarioReducer = scenarioSlice.reducer;
532
+
533
+ // src/store.ts
534
+ var createTestGameStore = () => configureStore({
535
+ reducer: {
536
+ npcs: npcsReducer,
537
+ player: playerReducer,
538
+ grid: gridReducer,
539
+ stealth: stealthReducer,
540
+ social: socialReducer,
541
+ bridge: bridgeReducer,
542
+ memory: memoryReducer,
543
+ inventory: inventoryReducer,
544
+ soul: soulReducer,
545
+ ui: uiReducer,
546
+ transcript: transcriptReducer,
547
+ scenario: scenarioReducer,
548
+ harness: harnessReducer
549
+ }
550
+ });
551
+
552
+ // src/game.ts
553
+ var nowId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
554
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
555
+ var applyScenarioEvent = async (step, store) => {
556
+ if (step.eventType === "stealth") {
557
+ store.dispatch(stealthActions.setDoorOpen(true));
558
+ store.dispatch(stealthActions.bumpAlert(25));
559
+ store.dispatch(
560
+ npcsActions.upsertNPC({
561
+ id: "doomguard",
562
+ name: "Doomguard Patrol",
563
+ faction: "Doomguards",
564
+ hp: 100,
565
+ suspicion: 40,
566
+ inventory: [],
567
+ knownSecrets: [],
568
+ position: { x: 5, y: 10 }
569
+ })
570
+ );
571
+ const movement = capMoveDistance(store.getState().bridge, 6);
572
+ store.dispatch(npcsActions.moveNPC({ id: "doomguard", position: { x: 5, y: 10 + movement.allowedDistance } }));
573
+ store.dispatch(
574
+ memoryActions.storeMemory({
575
+ npcId: "doomguard",
576
+ text: "Armory door found open at x:5, y:12",
577
+ importance: 0.9
578
+ })
579
+ );
580
+ }
581
+ if (step.eventType === "social") {
582
+ store.dispatch(
583
+ npcsActions.upsertNPC({
584
+ id: "miller",
585
+ name: "Miller",
586
+ faction: "Neutral",
587
+ hp: 100,
588
+ suspicion: 50,
589
+ inventory: ["medkit"],
590
+ knownSecrets: ["player_stole_rations"],
591
+ position: { x: 5, y: 12 }
592
+ })
593
+ );
594
+ store.dispatch(
595
+ socialActions.setDialogue("I know you took those rations. I'm not giving you this medkit unless you pay double.")
596
+ );
597
+ store.dispatch(socialActions.setTradeOffer({ npcId: "miller", item: "medkit", price: 100 }));
598
+ store.dispatch(npcsActions.patchNPC({ id: "miller", patch: { suspicion: 75 } }));
599
+ store.dispatch(bridgeActions.loadBridgePreset("social"));
600
+ }
601
+ if (step.eventType === "escape") {
602
+ store.dispatch(playerActions.setHidden(false));
603
+ const jump = validateJump(store.getState().bridge, 800);
604
+ if (!jump.valid) {
605
+ store.dispatch(uiActions.addMessage(`Bridge blocked jump: ${jump.reason}`));
606
+ store.dispatch(npcsActions.patchNPC({ id: "doomguard", patch: { hp: 90 } }));
607
+ }
608
+ for (let i = 0; i < 3; i += 1) {
609
+ store.dispatch(stealthActions.bumpAlert(10));
610
+ await delay(300);
611
+ }
612
+ }
613
+ if (step.eventType === "persistence") {
614
+ store.dispatch(soulActions.markSoulExported({ npcId: "doomguard", txId: "tx-demo-001" }));
615
+ store.dispatch(memoryActions.clearMemoryForNpc("doomguard"));
616
+ store.dispatch(soulActions.markSoulImported("tx-demo-001"));
617
+ }
618
+ };
619
+ var runGame = async ({
620
+ mode,
621
+ execute
622
+ }) => {
623
+ const store = createTestGameStore();
624
+ store.dispatch(uiActions.setMode(mode));
625
+ store.dispatch(
626
+ npcsActions.upsertNPC({
627
+ id: "scout",
628
+ name: "Scout",
629
+ faction: "Player",
630
+ hp: 100,
631
+ suspicion: 0,
632
+ inventory: ["coin-pouch"],
633
+ knownSecrets: [],
634
+ position: { x: 1, y: 1 }
635
+ })
636
+ );
637
+ const rl = mode === "manual" ? createInterface({ input, output }) : null;
638
+ for (const step of store.getState().scenario.steps) {
639
+ if (mode === "manual") {
640
+ console.log(`
641
+ == ${step.title} ==
642
+ ${step.description}
643
+ `);
644
+ console.log(renderGrid(store.getState()));
645
+ await rl?.question("Press Enter to run this scenario step...");
646
+ } else {
647
+ console.log(`
648
+ == ${step.title} ==`);
649
+ console.log(step.description);
650
+ }
651
+ await applyScenarioEvent(step, store);
652
+ for (const command of step.commands) {
653
+ const result = await runCommand(command, execute);
654
+ store.dispatch(harnessActions.markCovered(command.group));
655
+ store.dispatch(
656
+ transcriptActions.recordTranscript({
657
+ id: nowId(),
658
+ scenarioId: step.id,
659
+ commandGroup: command.group,
660
+ command: command.command,
661
+ expectedRoutes: command.expectedRoutes,
662
+ status: result.status,
663
+ output: result.output,
664
+ at: (/* @__PURE__ */ new Date()).toISOString()
665
+ })
666
+ );
667
+ console.log(`[${result.status}] ${command.command}`);
668
+ }
669
+ }
670
+ rl?.close();
671
+ const state = store.getState();
672
+ const missingGroups = selectMissingGroups(state.harness.covered);
673
+ const complete = missingGroups.length === 0;
674
+ const summary = complete ? `Run complete: all ${Object.keys(state.harness.covered).length} command groups were exercised.` : `Run incomplete: missing command groups -> ${missingGroups.join(", ")}`;
675
+ console.log("\n=== Transcript Summary ===");
676
+ for (const entry of state.transcript.entries) {
677
+ console.log(
678
+ `${entry.at} | ${entry.commandGroup} | ${entry.status} | ${entry.command} | routes: ${entry.expectedRoutes.join(", ")}`
679
+ );
680
+ }
681
+ console.log(`
682
+ ${summary}`);
683
+ return {
684
+ complete,
685
+ missingGroups,
686
+ transcript: state.transcript.entries,
687
+ summary
688
+ };
689
+ };
690
+
691
+ export {
692
+ REQUIRED_GROUPS,
693
+ runGame
694
+ };
package/dist/cli.d.mts ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runs the CLI entry flow and exits with status code:
4
+ * - `0` when coverage is complete
5
+ * - `2` when required command groups are missing
6
+ * User Story: As a CI user, I want exit codes to reflect coverage completeness so failures are actionable.
7
+ */
8
+ declare const runCli: () => Promise<void>;
9
+ /**
10
+ * Test-only exports for direct unit coverage of internal CLI helpers.
11
+ */
12
+ declare const __test: {
13
+ usage: () => void;
14
+ getArgValue: (name: string) => string | undefined;
15
+ };
16
+
17
+ export { __test, runCli };