pwn 0.5.759 → 0.5.760

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ef7228011e10bd0573cab148f192e8ef963608a002d4e3d833fcd2e380fd973
4
- data.tar.gz: 8f238e16e7d0d521deb4b4abdad45fed2d15fb82d92bcc8448b0877bc23b8783
3
+ metadata.gz: 2115a19977c0539855a44ecea57a2fb74bc48ae80ff3a713d2c08b95bdb04e55
4
+ data.tar.gz: 3c237b68883205d53dee7911e18be1e5b5454184f27c7d093e655e75611f5184
5
5
  SHA512:
6
- metadata.gz: d6eb621a41250c5332e00f941c8b6b7444f796fcb30c009d774d7a0814f3c268809152275f2c63f7f7076450c75bbe662c0248ec6b21abb9730b44b097fdb139
7
- data.tar.gz: 8ea184fdfa2018bc59f00aef8585d190907236a3d71df302c21db3eb025813111cd5a7bb289513c5a7f51a816f1c7e094f5b6c0bf420ff50d9a7a27a11ffbc65
6
+ metadata.gz: 01c9e98a32745f26186d2f664d62f7d1801eea05a56ffe149ea20b84aaef383d5d46d909e5be97247454cedad4781c3ce393783dcc64707a07bbd818a37872c5
7
+ data.tar.gz: 562ea7604c6e23cbe128864aca11338e9c60bd7d83bd5e54692c956f136f29f036e1fb7b63c6cdb9f0defd3168c616cfff5c9194f169b9df209094952e0ae66b
data/Gemfile CHANGED
@@ -49,7 +49,7 @@ gem 'jwt', '3.3.0'
49
49
  gem 'libusb', '0.8.0'
50
50
  gem 'luhn', '3.0.0'
51
51
  gem 'mail', '2.9.1'
52
- gem 'mcp', '1.6.1'
52
+ gem 'mcp', '1.7.0'
53
53
  gem 'meshtastic', '0.0.186'
54
54
  gem 'metasm', '1.0.6'
55
55
  gem 'mongo', '2.26.0'
@@ -59,8 +59,78 @@ module PWN
59
59
  # (:escalation_persona) — the local model still produces the final
60
60
  # answer so Learning/Metrics stay attributed to :ollama.
61
61
  module Loop
62
+ # Provider calls run on a child. Thread#raise does not wake a native read,
63
+ # so the owner waits in Ruby and leaves within one poll on cancel.
64
+ class ProviderWindow
65
+ def request_model_cancel
66
+ @mutex.synchronize do
67
+ @cancel_requested = true
68
+ if @phase == :model && !@model_signal_sent
69
+ @model_signal_sent = true
70
+ @owner.raise(Steering::ModelCancelled)
71
+ end
72
+ end
73
+ end
74
+
75
+ def model(messages:, &block)
76
+ result = nil
77
+ worker = nil
78
+ Thread.handle_interrupt(Steering::ModelCancelled => :never) do
79
+ checkpoint(messages: messages, phase: :model)
80
+ raise Steering::ModelCancelled if cancel_requested?
81
+
82
+ worker = provider_thread(&block)
83
+ Thread.handle_interrupt(Steering::ModelCancelled => :immediate) do
84
+ loop do
85
+ raise Steering::ModelCancelled if cancel_requested?
86
+
87
+ break if worker.join(0.05)
88
+ end
89
+ end
90
+ result = worker.value
91
+ ensure
92
+ abandon_provider(worker)
93
+ @mutex.synchronize { @phase = :boundary }
94
+ begin
95
+ Thread.handle_interrupt(Steering::ModelCancelled => :immediate) { nil }
96
+ rescue Steering::ModelCancelled
97
+ nil
98
+ end
99
+ end
100
+ result
101
+ rescue Steering::ModelCancelled
102
+ nil
103
+ end
104
+
105
+ private
106
+
107
+ def cancel_requested?
108
+ @mutex.synchronize { @cancel_requested }
109
+ end
110
+
111
+ def provider_thread(&block)
112
+ parent = Thread.current
113
+ locals = parent.keys.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, parent[key]] }
114
+ variables = parent.thread_variables.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, parent.thread_variable_get(key)] }
115
+ Thread.new do
116
+ Thread.current.report_on_exception = false
117
+ locals.each { |key, value| Thread.current[key] = value }
118
+ variables.each { |key, value| Thread.current.thread_variable_set(key, value) }
119
+ Thread.handle_interrupt(Steering::ModelCancelled => :never, &block)
120
+ end
121
+ end
122
+
123
+ def abandon_provider(worker)
124
+ return unless worker&.alive?
125
+
126
+ worker.raise(Steering::ModelCancelled)
127
+ worker.join(0.2)
128
+ worker.kill if worker.alive?
129
+ end
130
+ end
131
+
62
132
  # Request-owned input broker; only the provider window is interruptible.
63
- class Steering
133
+ class Steering < ProviderWindow
64
134
  INPUT_LOCK = Mutex.new
65
135
  INPUT_OWNERS = {} # rubocop:disable Style/MutableConstant -- guarded by INPUT_LOCK
66
136
 
@@ -80,12 +150,14 @@ module PWN
80
150
  attr_reader :reader
81
151
 
82
152
  def initialize(input:, output:)
153
+ super()
83
154
  @input = input
84
155
  @output = output
85
156
  @owner = Thread.current
86
157
  @mutex = Mutex.new
87
158
  @pending = []
88
159
  @phase = :boundary
160
+ @cancel_requested = false
89
161
  end
90
162
 
91
163
  def submit(text)
@@ -127,23 +199,6 @@ module PWN
127
199
  raise Restart.new(messages, instructions)
128
200
  end
129
201
 
130
- def model(messages:, &block)
131
- # Drain an in-flight signal inside this rescue, never in a tool.
132
- Thread.handle_interrupt(ModelCancelled => :never) do
133
- checkpoint(messages: messages, phase: :model)
134
- Thread.handle_interrupt(ModelCancelled => :immediate, &block)
135
- ensure
136
- @mutex.synchronize { @phase = :boundary }
137
- begin
138
- Thread.handle_interrupt(ModelCancelled => :immediate) { nil }
139
- rescue ModelCancelled
140
- nil
141
- end
142
- end
143
- rescue ModelCancelled
144
- nil
145
- end
146
-
147
202
  def with_reader
148
203
  require 'io/console'
149
204
  input_id = @input.fileno
data/lib/pwn/banner.rb CHANGED
@@ -187,6 +187,59 @@ module PWN
187
187
  end
188
188
  end
189
189
 
190
+ # Caller-owned simulation: yields on the UI thread, never reads input or
191
+ # accumulates a replay. The caller supplies the clock and normalized keys.
192
+ class MiniGame
193
+ def initialize(name:, width:, height:, seed: MINI_SEED)
194
+ @name = name.to_s
195
+ raise ArgumentError, "Unknown mini-banner: #{name}" unless Banner.mini_names.map(&:to_s).include?(@name)
196
+
197
+ limit = @name == 'falling_blocks' ? 64 : MINI_WIDTH
198
+ @width = width.clamp(5, limit)
199
+ @height = height.clamp(5, limit)
200
+ @keys = []
201
+ @simulation = Fiber.new do
202
+ options = { frames: self, width: @width, height: @name == 'falling_blocks' ? @height * 2 : @height,
203
+ rng: Random.new(seed), player: self }
204
+ Banner.send("mini_simulate_#{@name}", options)
205
+ end
206
+ end
207
+
208
+ def key(key)
209
+ @keys << key if @keys.length < 16
210
+ end
211
+
212
+ def controls
213
+ keys = @keys
214
+ @keys = []
215
+ keys
216
+ end
217
+
218
+ def step
219
+ @simulation.resume
220
+ end
221
+
222
+ def cells(frame)
223
+ rows = frame[:rows].drop(1)
224
+ rows = Banner.send(:mini_block_board, board: rows) if @name == 'falling_blocks'
225
+ rows = Banner.send(:mini_quarter_board, board: rows, name: @name) if @name == 'snake'
226
+ rows.map do |row|
227
+ row.chars.map do |pixel|
228
+ glyph, foreground, background = MINI_PIXELS.fetch(pixel)
229
+ { glyph: glyph.dup, foreground: foreground, background: background || :black }
230
+ end
231
+ end
232
+ end
233
+
234
+ def <<(frame)
235
+ Fiber.yield(frame)
236
+ end
237
+
238
+ def length
239
+ 0
240
+ end
241
+ end
242
+
190
243
  # Six immutable, compact replays bound memory even when callers resize/reseed.
191
244
  private_class_method def self.mini_replay(opts = {})
192
245
  key = [opts[:name].to_s.dup.freeze, opts[:width], opts[:height], opts[:seed]].freeze
@@ -347,56 +400,99 @@ module PWN
347
400
  bag = (0...MINI_SHAPES.length).to_a.shuffle(random: rng) if bag.empty?
348
401
  type = bag.shift
349
402
  shape = MINI_SHAPES[type]
350
- landing = mini_landing(board: board, shape: shape, rng: rng)
403
+ landing = opts[:player] ? {} : mini_landing(board: board, shape: shape, rng: rng)
351
404
  x = (width - shape.map(&:first).max - 1) / 2
352
405
  unless landing && mini_fits?(board: board, shape: shape, x: x, y: 0)
353
406
  mini_reset_board(frames: frames, board: board)
354
407
  board = Array.new(height) { ' ' * width }
355
408
  next
356
409
  end
357
- rotations = mini_rotations(shape: shape)
358
- plan = [[shape, x, 0]]
359
- blocked = false
360
- (1..landing[:rotation]).each do |rotation|
361
- shape = rotations[rotation]
362
- if mini_fits?(board: board, shape: shape, x: x, y: 0)
363
- plan << [shape, x, 0]
364
- else
365
- blocked = true
410
+ if opts[:player]
411
+ shape, x, y = mini_play_piece(board: board, type: type, frames: frames, player: opts[:player])
412
+ landing = { x: x, y: y }
413
+ else
414
+ shape, x = mini_block_plan(board: board, shape: shape, x: x, landing: landing, frames: frames, type: type)
415
+ unless shape
416
+ mini_reset_board(frames: frames, board: board)
417
+ board = Array.new(height) { ' ' * width }
418
+ next
366
419
  end
367
420
  end
368
- until x == landing[:x] || blocked
369
- x += landing[:x] <=> x
370
- blocked = !mini_fits?(board: board, shape: shape, x: x, y: 0)
371
- plan << [shape, x, 0] unless blocked
372
- end
373
- if blocked
374
- mini_reset_board(frames: frames, board: board)
375
- board = Array.new(height) { ' ' * width }
376
- next
377
- end
378
- (1..landing[:y]).each { |y| plan << [shape, x, y] }
379
421
  color = %w[c y m b r g g][type]
380
- plan.each do |piece, px, py|
381
- picture = board.map(&:dup)
382
- active = piece.map { |dx, dy| [px + dx, py + dy] }
383
- active.each { |ax, ay| picture[ay][ax] = color }
384
- mini_emit(frames: frames, board: picture,
385
- state: { active: active, stack: board.map(&:dup), type: type })
386
- end
387
422
  shape.each { |dx, dy| board[landing[:y] + dy][x + dx] = color }
388
423
  mini_emit(frames: frames, board: board, event: :lock, ticks: 2)
389
- full = board.each_index.reject { |y| board[y].include?(' ') }
424
+ full = board.each_index.reject { |row| board[row].include?(' ') }
390
425
  next if full.empty?
391
426
 
392
427
  flash = board.map(&:dup)
393
- full.each { |y| flash[y] = 'w' * width }
428
+ full.each { |row| flash[row] = 'w' * width }
394
429
  mini_emit(frames: frames, board: flash, event: :clear, state: { lines: full }, ticks: 3)
395
- board = Array.new(full.length) { ' ' * width } + board.each_with_index.filter_map { |row, y| row unless full.include?(y) }
430
+ board = Array.new(full.length) { ' ' * width } + board.each_with_index.filter_map { |row, index| row unless full.include?(index) }
396
431
  mini_emit(frames: frames, board: board, event: :collapse, ticks: 2)
397
432
  end
398
433
  end
399
434
 
435
+ private_class_method def self.mini_block_plan(opts = {})
436
+ board, shape, x, landing, frames, type = opts.values_at(:board, :shape, :x, :landing, :frames, :type)
437
+ rotations = mini_rotations(shape: shape)
438
+ plan = [[shape, x, 0]]
439
+ blocked = false
440
+ (1..landing[:rotation]).each do |rotation|
441
+ shape = rotations[rotation]
442
+ if mini_fits?(board: board, shape: shape, x: x, y: 0)
443
+ plan << [shape, x, 0]
444
+ else
445
+ blocked = true
446
+ end
447
+ end
448
+ until x == landing[:x] || blocked
449
+ x += landing[:x] <=> x
450
+ blocked = !mini_fits?(board: board, shape: shape, x: x, y: 0)
451
+ plan << [shape, x, 0] unless blocked
452
+ end
453
+ return if blocked
454
+
455
+ (1..landing[:y]).each { |y| plan << [shape, x, y] }
456
+ color = %w[c y m b r g g][type]
457
+ plan.each do |piece, px, py|
458
+ picture = board.map(&:dup)
459
+ active = piece.map { |dx, dy| [px + dx, py + dy] }
460
+ active.each { |ax, ay| picture[ay][ax] = color }
461
+ mini_emit(frames: frames, board: picture,
462
+ state: { active: active, stack: board.map(&:dup), type: type })
463
+ end
464
+ [shape, x]
465
+ end
466
+
467
+ private_class_method def self.mini_play_piece(opts = {})
468
+ board, type, frames, player = opts.values_at(:board, :type, :frames, :player)
469
+ shape = MINI_SHAPES[type]
470
+ x = (board.first.length - shape.map(&:first).max - 1) / 2
471
+ y = 0
472
+ tick = 0
473
+ loop do
474
+ active = shape.map { |dx, dy| [x + dx, y + dy] }
475
+ picture = board.map(&:dup)
476
+ active.each { |ax, ay| picture[ay][ax] = %w[c y m b r g g][type] }
477
+ mini_emit(frames: frames, board: picture, state: { active: active, stack: board.map(&:dup), type: type })
478
+ keys = player.controls
479
+ keys.each do |key|
480
+ candidate = key == ' ' ? mini_rotations(shape: shape).fetch(1, shape) : shape
481
+ nx = x + { left: -1, right: 1 }.fetch(key, 0)
482
+ next unless mini_fits?(board: board, shape: candidate, x: nx, y: y)
483
+
484
+ shape = candidate
485
+ x = nx
486
+ end
487
+ tick += 1
488
+ next unless keys.include?(:down) || (tick % 5).zero?
489
+ break unless mini_fits?(board: board, shape: shape, x: x, y: y + 1)
490
+
491
+ y += 1
492
+ end
493
+ [shape, x, y]
494
+ end
495
+
400
496
  private_class_method def self.mini_reset_board(opts = {})
401
497
  board = opts[:board].map { |row| row.gsub(/[^ ]/, 'w') }
402
498
  mini_emit(frames: opts[:frames], board: board, event: :game_over, ticks: 8)
@@ -442,19 +538,31 @@ module PWN
442
538
  height = opts[:height]
443
539
  rng = opts[:rng]
444
540
  spaces = (0...height).flat_map { |y| (0...width).map { |x| [x, y] } }
541
+ player = opts[:player]
445
542
  while frames.length < MINI_FRAME_COUNT
446
543
  y = rng.rand(height)
447
544
  body = [[2, y], [1, y], [0, y]]
545
+ direction = [1, 0]
448
546
  food = (spaces - body).sample(random: rng)
449
547
  steps = 0
450
548
  loop do
451
549
  board = Array.new(height) { ' ' * width }
452
550
  body.each_with_index { |(x, row), index| board[row][x] = index.zero? ? 'y' : 'g' }
453
551
  board[food[1]][food[0]] = 'f' if food
454
- mini_emit(frames: frames, board: board, ticks: 2, state: { body: body.map(&:dup), food: food&.dup })
552
+ mini_emit(frames: frames, board: board, ticks: player ? 1 : 2, state: { body: body.map(&:dup), food: food&.dup })
455
553
  break if frames.length >= MINI_FRAME_COUNT
456
554
 
457
- target = mini_snake_route(body: body, food: food, width: width, height: height, rng: rng) if food
555
+ if player
556
+ turn = player.controls.filter_map { |key| { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }[key] }.find do |dx, dy|
557
+ [dx, dy] != direction && [dx, dy] != direction.map(&:-@)
558
+ end
559
+ direction = turn if turn
560
+ target = [body.first[0] + direction[0], body.first[1] + direction[1]]
561
+ occupied = target == food ? body : body.take(body.length - 1)
562
+ target = nil unless spaces.include?(target) && !occupied.include?(target)
563
+ elsif food
564
+ target = mini_snake_route(body: body, food: food, width: width, height: height, rng: rng)
565
+ end
458
566
  if !food || !target || steps >= width * height * 4
459
567
  mini_reset_board(frames: frames, board: board)
460
568
  break
@@ -477,6 +585,7 @@ module PWN
477
585
  height = opts[:height]
478
586
  rng = opts[:rng]
479
587
  paddles = [(height - 1) / 2.0, (height - 1) / 2.0]
588
+ player = opts[:player]
480
589
  scores = [0, 0]
481
590
  while frames.length < MINI_FRAME_COUNT
482
591
  x = (width - 1) / 2.0
@@ -485,9 +594,14 @@ module PWN
485
594
  vy = ((rng.rand * 0.4) + 0.1) * (rng.rand(2).zero? ? -1 : 1)
486
595
  bias = Array.new(2) { rng.rand(5).zero? ? rng.rand(-2..2) : rng.rand - 0.5 }
487
596
  rally = 0
488
- mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: :serve, ticks: 16)
597
+ mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: :serve, ticks: player ? 1 : 16)
489
598
  loop do
599
+ player&.controls&.each do |key|
600
+ paddles[0] = (paddles[0] + { up: -0.5, down: 0.5 }.fetch(key, 0)).clamp(0, height - 1)
601
+ end
490
602
  paddles.each_index do |side|
603
+ next if player && side.zero?
604
+
491
605
  incoming = side.zero? ? vx.negative? : vx.positive?
492
606
  target = incoming ? y - 0.25 + bias[side] : (height - 1) / 2.0
493
607
  speed = incoming ? 0.275 : 0.11
@@ -519,7 +633,7 @@ module PWN
519
633
  y = ny
520
634
  if x.negative? || x > width - 1
521
635
  scores[x.negative? ? 1 : 0] += 1
522
- mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: nil, scores: scores, event: :score, ticks: 20)
636
+ mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: nil, scores: scores, event: :score, ticks: player ? 1 : 20)
523
637
  break
524
638
  end
525
639
  mini_pong_snapshot(frames: frames, width: width, height: height, paddles: paddles, ball: [x, y], scores: scores, event: event, rally: rally)
@@ -594,13 +708,18 @@ module PWN
594
708
  end
595
709
  angle = Math.atan2(mini_delta(from: ship[1], to: target[1], span: height), mini_delta(from: ship[0], to: target[0], span: width))
596
710
  turn = mini_delta(from: ship[2], to: angle, span: Math::PI * 2).clamp(-0.1, 0.1)
711
+ keys = opts[:player]&.controls
712
+ turn = keys.sum { |key| { left: -0.2, right: 0.2 }.fetch(key, 0) } if keys
597
713
  ship[2] = (ship[2] + turn) % (Math::PI * 2)
598
- thrust = tick % 80 < 24
714
+ power = tick % 80 < 24 ? 1 : 0
715
+ power = keys.sum { |key| { up: 1, down: -1 }.fetch(key, 0) }.clamp(-1, 1) if keys
716
+ thrust = !power.zero?
599
717
  [Math.cos(ship[2]), Math.sin(ship[2])].each_with_index do |direction, axis|
600
- velocity[axis] = ((velocity[axis] * 0.99) + (thrust ? direction * 0.006 : 0)).clamp(-0.15, 0.15)
718
+ velocity[axis] = ((velocity[axis] * 0.99) + (direction * 0.006 * power)).clamp(-0.15, 0.15)
601
719
  ship[axis] = (ship[axis] + velocity[axis]) % (axis.zero? ? width : height)
602
720
  end
603
- shots << [ship[0], ship[1], Math.cos(ship[2]) * 0.36, Math.sin(ship[2]) * 0.36, 24] if (tick % 12).zero?
721
+ fire = keys ? keys.include?(' ') : (tick % 12).zero?
722
+ shots << [ship[0], ship[1], Math.cos(ship[2]) * 0.36, Math.sin(ship[2]) * 0.36, 24] if fire
604
723
  shots.each do |shot|
605
724
  shot[0] = (shot[0] + shot[2]) % width
606
725
  shot[1] = (shot[1] + shot[3]) % height
@@ -86,6 +86,7 @@ module PWN
86
86
 
87
87
  def stop
88
88
  @stopping = true
89
+ request_model_cancel
89
90
  submit('Stop the active request.')
90
91
  end
91
92
 
@@ -524,13 +525,12 @@ module PWN
524
525
 
525
526
  def cancel
526
527
  if busy?
527
- return if @cancelling
528
-
528
+ first = !@cancelling
529
529
  @cancelling = true
530
530
  @control&.stop
531
531
  @swarm&.cancel_all
532
532
  @tool_input.close unless @tool_input.closed?
533
- add(:warning, 'Cancellation requested. Waiting for the active tool to finish; work already started is not undone.')
533
+ add(:warning, 'Cancellation requested. Waiting for a safe boundary; work already started is not undone.') if first
534
534
  else
535
535
  @editor.replace('')
536
536
  end
@@ -681,6 +681,7 @@ module PWN
681
681
  def handle(key)
682
682
  return if key.nil?
683
683
  return handle_system_role(key) if @role_editor && (!@too_small || ["\e", "\u0003", "\u0004", :eof].include?(key))
684
+ return cancel if key == "\u0003" && busy?
684
685
 
685
686
  return if @too_small && ![:eof, "\u0003", "\u0004"].include?(key)
686
687
  return cancel if @too_small && key == "\u0003" && !@model_prompt
@@ -714,7 +715,10 @@ module PWN
714
715
 
715
716
  if ["\u0018", "\u0014"].include?(key)
716
717
  @menu = nil
717
- @focus = @focus == :session ? :mission : :session
718
+ panes = @header_pane_height ? %i[mission session animation] : %i[mission session]
719
+ @focus = panes[(panes.index(@focus).to_i + 1) % panes.length]
720
+ @banner_game&.controls
721
+ @banner_game_tick = nil
718
722
  return
719
723
  end
720
724
 
@@ -726,6 +730,8 @@ module PWN
726
730
  @menu = nil
727
731
  return
728
732
  end
733
+ return handle_animation(key) if @focus == :animation && ![:eof, "\u0003", "\u0004"].include?(key)
734
+
729
735
  case key
730
736
  when :eof, "\u0004"
731
737
  @leaving = true
@@ -794,6 +800,24 @@ module PWN
794
800
  end
795
801
  end
796
802
 
803
+ def handle_animation(key)
804
+ if key == :paste_start
805
+ @focus = :mission
806
+ @paste = true
807
+ @banner_game&.controls
808
+ elsif [:up, :down, :left, :right, ' '].include?(key)
809
+ @banner_game&.key(key)
810
+ elsif ["\r", "\n", "\e", :newline].include?(key)
811
+ @focus = :mission
812
+ @banner_game&.controls
813
+ elsif key.is_a?(String) && key.ord >= 32
814
+ @focus = :mission
815
+ @banner_game&.controls
816
+ @editor.insert(key)
817
+ refresh_completion
818
+ end
819
+ end
820
+
797
821
  def open_system_role
798
822
  return add(:warning, 'System role cannot change while a request or swarm job is running.') if busy?
799
823
 
@@ -1201,6 +1225,7 @@ module PWN
1201
1225
  end
1202
1226
  cadence = cells ? @banner_frame_seconds : PWN::Banner::MINI_FRAME_SECONDS
1203
1227
  frame = ((now - @banner_started) / cadence).floor % PWN::Banner::MINI_FRAME_COUNT
1228
+ return playable_banner(size, now) if cells && @focus == :animation
1204
1229
  return PWN::Banner.mini_cells(name: @banner_name, frame: frame, width: size, height: size, seed: @banner_seed, branding: false) if cells
1205
1230
 
1206
1231
  art = PWN::Banner.mini_frame(name: @banner_name, frame: frame, width: size, height: size, branding: false)
@@ -1210,6 +1235,21 @@ module PWN
1210
1235
  canvas
1211
1236
  end
1212
1237
 
1238
+ def playable_banner(size, now)
1239
+ key = [@banner_session, @banner_name, size, @banner_seed]
1240
+ if @banner_game_key != key
1241
+ @banner_game_key = key
1242
+ @banner_game = PWN::Banner::MiniGame.new(name: @banner_name, width: size, height: size, seed: @banner_seed)
1243
+ @banner_game_tick = nil
1244
+ end
1245
+ tick = (now / @banner_frame_seconds).floor
1246
+ if @banner_game_tick != tick
1247
+ @banner_game_cells = @banner_game.cells(@banner_game.step)
1248
+ @banner_game_tick = tick
1249
+ end
1250
+ @banner_game_cells
1251
+ end
1252
+
1213
1253
  def draw_banner(size)
1214
1254
  cells = banner_frame(size, cells: true)
1215
1255
  top = [(size - cells.length) / 2, 0].max
@@ -1267,6 +1307,11 @@ module PWN
1267
1307
 
1268
1308
  def footer_text
1269
1309
  text = '/ menu · ^C=cancel · ^D=back · ^L=clear · ^T=toggle pane · ^S=swarm · ^R=search ^O=operations · ↑↓ history · Shift+Enter=newline · Enter=send'
1310
+ if @focus == :animation
1311
+ controls = { pong: '↑↓ paddle', snake: '↑↓←→ steer', falling_blocks: '←→ move · ↓ faster · Space=rotate',
1312
+ asteroids: '←→ turn · ↑↓ thrust/reverse · Space=fire' }.fetch(@banner_name, '↑↓←→ play')
1313
+ return text.sub('↑↓ history', controls).sub('Shift+Enter=newline · Enter=send', 'Enter/Esc=draft')
1314
+ end
1270
1315
  @focus == :session ? text.sub('↑↓ history', '↑↓ scroll · HOME · PGUP · PGDN · END') : text
1271
1316
  end
1272
1317
 
@@ -1295,6 +1340,7 @@ module PWN
1295
1340
  visible.each_with_index { |spans, index| put_spans(2 + index, @header_text_column, spans) }
1296
1341
  if @header_pane_height
1297
1342
  box(0, 0, height, height, '')
1343
+ mark_active(0, 0, height, 'PLAY') if @focus == :animation
1298
1344
  draw_banner(height - 2)
1299
1345
  end
1300
1346
  height
@@ -1338,6 +1384,8 @@ module PWN
1338
1384
  end
1339
1385
  engine = PWN::Env.dig(:ai, :active).to_s
1340
1386
  model = REPL.pwn_ai_engine_model(engine: engine)
1387
+ header_layout(engine, model)
1388
+ @focus = :mission if @focus == :animation && !@header_pane_height
1341
1389
  footer, compose_height, footer_height = layout_chrome
1342
1390
  @compose_height = compose_height
1343
1391
  @footer_height = footer_height
@@ -1359,9 +1407,9 @@ module PWN
1359
1407
  rows.slice(start, @page_size).to_a.each_with_index { |(color, row), index| put(header_height + 1 + index, 2, row, color) }
1360
1408
  draw_sidebar(header_height, pane_width + 1, timeline_height) if sidebar.positive?
1361
1409
  compose_y = @height - compose_height - footer_height
1362
- mission_title = @focus == :session ? 'MISSION CONTROL' : 'MISSION CONTROL · active'
1410
+ mission_title = @focus == :mission ? 'MISSION CONTROL · active' : 'MISSION CONTROL'
1363
1411
  box(compose_y, 0, compose_height, @width - 1, mission_title)
1364
- mark_active(compose_y, 0, @width - 1, mission_title) if @focus != :session
1412
+ mark_active(compose_y, 0, @width - 1, mission_title) if @focus == :mission
1365
1413
  draw_composer(compose_y, height: compose_height)
1366
1414
  footer_top = @height - footer_height
1367
1415
  footer.first(footer_height).each_with_index { |line, index| put(footer_top + index, 1, line, tone(:footer)) }
@@ -1370,7 +1418,9 @@ module PWN
1370
1418
  draw_details(engine, model) if @details
1371
1419
  draw_swarm if @workspace
1372
1420
  draw_system_role if @role_editor
1373
- if @focus == :session && !(@details || @workspace || @role_editor)
1421
+ if @focus == :animation && !(@details || @workspace || @role_editor || @model_prompt)
1422
+ @screen.setpos(1, 1)
1423
+ elsif @focus == :session && !(@details || @workspace || @role_editor)
1374
1424
  @screen.setpos(header_height + 1, 2)
1375
1425
  elsif @role_editor || !(@details || @workspace)
1376
1426
  @screen.setpos(*@cursor_position)
@@ -15,6 +15,7 @@ module PWN
15
15
 
16
16
  def stop
17
17
  @stopping = true
18
+ request_model_cancel
18
19
  submit('Stop this swarm job.')
19
20
  end
20
21
 
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.759'
4
+ VERSION = '0.5.760'
5
5
  end
@@ -228,7 +228,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
228
228
  calls = []
229
229
  owner = Thread.current
230
230
  allow(described_class).to receive(:call_engine) do |opts|
231
- expect(Thread.current).to eq(owner)
231
+ expect(Thread.current).not_to eq(owner)
232
232
  calls << Marshal.load(Marshal.dump(opts[:messages]))
233
233
  if calls.length == 1
234
234
  entered << true
@@ -25,7 +25,103 @@ describe PWN::Banner do
25
25
  end
26
26
  end
27
27
  end
28
+ end
29
+
30
+ describe PWN::Banner::MiniGame do
31
+ it 'renders bounded, unbranded colored frames without accumulating a replay' do
32
+ PWN::Banner.mini_names.product([5, 8, 16]).each do |name, size|
33
+ game = described_class.new(name: name, width: size, height: size)
34
+ 40.times do
35
+ frame = game.step
36
+ cells = game.cells(frame)
37
+ expect(cells.length).to eq(size)
38
+ expect(cells.map(&:length)).to all(eq(size))
39
+ expect(cells.flatten.map { |cell| cell[:glyph] }.join).not_to match(/[PWN]/)
40
+ expect(cells.flatten).to all(include(:foreground, :background))
41
+ end
42
+ expect(game.length).to eq(0)
43
+ end
44
+ end
45
+
46
+ it 'lets the player move only the left Pong paddle while the ball and opponent keep playing' do
47
+ game = PWN::Banner::MiniGame.new(name: :pong, width: 8, height: 8, seed: 73)
48
+ first = game.step
49
+ game.key(:up)
50
+ moved = game.step
51
+ expect(moved[:state][:paddles][0]).to be < first[:state][:paddles][0]
52
+ expect(moved[:state][:ball]).not_to eq(first[:state][:ball])
53
+ game.key(:down)
54
+ expect(game.step[:state][:paddles][0]).to eq(first[:state][:paddles][0])
55
+ game.key(:left)
56
+ expect(game.step[:state][:paddles][0]).to eq(first[:state][:paddles][0])
57
+ 100.times do
58
+ game.key(:up)
59
+ game.step
60
+ end
61
+ expect(game.step[:state][:paddles][0]).to eq(0)
62
+ end
63
+
64
+ it 'steers Snake with arrows without reversing into its neck or changing direction twice per tick' do
65
+ game = PWN::Banner::MiniGame.new(name: :snake, width: 16, height: 16, seed: 73)
66
+ first = game.step[:state]
67
+ game.key(:left)
68
+ moved = game.step[:state]
69
+ expect(moved[:body].first).to eq([first[:body][0][0] + 1, first[:body][0][1]])
70
+ turn = moved[:body][0][1].positive? ? :up : :down
71
+ game.key(turn)
72
+ game.key(:left)
73
+ turned = game.step[:state]
74
+ expect(turned[:body].first).to eq([moved[:body][0][0], moved[:body][0][1] + (turn == :up ? -1 : 1)])
75
+ game.key(:left)
76
+ expect(game.step[:state][:body].first[0]).to eq(turned[:body][0][0] - 1)
77
+ end
78
+
79
+ it 'moves Tetris sideways, rotates with space and soft drops one row with down' do
80
+ game = PWN::Banner::MiniGame.new(name: :falling_blocks, width: 8, height: 8, seed: 73)
81
+ first = game.step[:state]
82
+ game.key(:left)
83
+ left = game.step[:state]
84
+ expect(left[:active]).to eq(first[:active].map { |x, y| [x - 1, y] })
85
+ game.key(:right)
86
+ expect(game.step[:state][:active]).to eq(first[:active])
87
+ game.key(' ')
88
+ rotated = game.step[:state]
89
+ expect(rotated[:active]).not_to eq(first[:active])
90
+ expect(rotated[:active].map(&:last).min).to eq(0)
91
+ game.key(:down)
92
+ dropped = game.step[:state]
93
+ expect(dropped[:active]).to eq(rotated[:active].map { |x, y| [x, y + 1] })
94
+ expect(dropped[:stack]).to eq(first[:stack])
95
+ events = 150.times.map do
96
+ game.key(:down)
97
+ game.step[:event]
98
+ end
99
+ expect(events).to include(:lock)
100
+ end
28
101
 
102
+ it 'turns and thrusts Asteroids only on arrows and fires only on space' do
103
+ game = PWN::Banner::MiniGame.new(name: :asteroids, width: 16, height: 16, seed: 73)
104
+ first = game.step[:state]
105
+ idle = game.step[:state]
106
+ expect(idle[:ship]).to eq(first[:ship])
107
+ expect(idle[:shots]).to be_empty
108
+ game.key(:left)
109
+ left = game.step[:state]
110
+ expect(left[:ship][2]).to be_within(0.001).of((first[:ship][2] - 0.2) % (Math::PI * 2))
111
+ game.key(:right)
112
+ expect(game.step[:state][:ship][2]).to be_within(0.001).of(first[:ship][2])
113
+ game.key(:up)
114
+ moved = game.step[:state]
115
+ expect(moved[:ship].take(2)).not_to eq(first[:ship].take(2))
116
+ expect(moved[:thrust]).to be(true)
117
+ game.key(:down)
118
+ expect(game.step[:state][:thrust]).to be(true)
119
+ game.key(' ')
120
+ expect(game.step[:state][:shots].length).to eq(1)
121
+ end
122
+ end
123
+
124
+ describe PWN::Banner do
29
125
  def block_colors(rows)
30
126
  rows.flat_map do |row|
31
127
  [0, 2].map do |bit|
@@ -258,6 +258,27 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
258
258
  foreign&.join
259
259
  end
260
260
 
261
+ it 'leaves a provider blocked in native IO without waiting for that read to return' do
262
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
263
+ allow(PWN::AI::Agent::Loop).to receive(:call_engine) do
264
+ Thread.handle_interrupt(Exception => :never) { sleep 8 }
265
+ end
266
+ keys = "what color is a lemon?\n".chars
267
+ cancelled = false
268
+ launch(keys) do
269
+ if !cancelled && @paint.any? { |row| row.include?('Elapsed') }
270
+ cancelled = true
271
+ "\u0003"
272
+ elsif @paint.any? { |row| row.include?('Request cancelled at a safe boundary') }
273
+ "\u0004"
274
+ else
275
+ Thread.pass
276
+ nil
277
+ end
278
+ end
279
+ expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 3
280
+ end
281
+
261
282
  it 'forwards tool input, preserves completed evidence before steering, and locks settings while running' do
262
283
  entered = Queue.new
263
284
  calls = 0
@@ -1025,6 +1046,86 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1025
1046
  console.draw
1026
1047
  expect(painted.map { |row| row[2] }.join).to include('SESSION · active')
1027
1048
  expect(screen).to have_received(:setpos).with(a_kind_of(Integer), 2)
1049
+ console.handle("\u0014")
1050
+ painted.clear
1051
+ console.draw
1052
+ expect(painted.map { |row| row[2] }.join).to include(' PLAY ')
1053
+ expect(painted.map { |row| row[2] }.join).not_to include('MISSION CONTROL · active')
1054
+ expect(screen).to have_received(:setpos).with(1, 1)
1055
+ allow(curses).to receive_messages(lines: 24, cols: 80)
1056
+ console.draw
1057
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
1058
+ end
1059
+
1060
+ it 'cycles mission, session and visible animation with both shortcuts without changing the draft' do
1061
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1062
+ editor = console.instance_variable_get(:@editor)
1063
+ editor.place('kept draft', 4)
1064
+ console.instance_variable_set(:@header_pane_height, 10)
1065
+ console.banner_frame(8, cells: true)
1066
+ console.handle("\u0014")
1067
+ console.handle("\u0018")
1068
+ expect(console.instance_variable_get(:@focus)).to eq(:animation)
1069
+ console.banner_frame(8, cells: true)
1070
+ game = console.instance_variable_get(:@banner_game)
1071
+ expect(game).to receive(:key).with(:up)
1072
+ expect(game).to receive(:key).with(' ')
1073
+ console.handle(:up)
1074
+ console.handle(' ')
1075
+ expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
1076
+ console.handle("\u0014")
1077
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
1078
+ expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
1079
+ end
1080
+
1081
+ it 'returns to the unchanged automatic animation when focus leaves the game' do
1082
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1083
+ allow(Process).to receive(:clock_gettime).and_return(10.0)
1084
+ automatic = console.banner_frame(8, cells: true)
1085
+ console.instance_variable_set(:@header_pane_height, 10)
1086
+ 2.times { console.handle("\u0014") }
1087
+ expect(console.banner_frame(8, cells: true)).not_to eq(automatic)
1088
+ console.handle("\u0018")
1089
+ expect(console.banner_frame(8, cells: true)).to eq(automatic)
1090
+ console.instance_variable_set(:@header_pane_height, nil)
1091
+ 2.times { console.handle("\u0014") }
1092
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
1093
+ end
1094
+
1095
+ it 'keeps modal, search and cancellation priority above animation controls' do
1096
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1097
+ console.instance_variable_set(:@focus, :animation)
1098
+ editor = console.instance_variable_get(:@editor)
1099
+ editor.place('draft', 2)
1100
+ game = double(key: nil)
1101
+ console.instance_variable_set(:@banner_game, game)
1102
+ console.instance_variable_set(:@details, 0)
1103
+ expect(console).to receive(:handle_details).with(:up)
1104
+ console.handle(:up)
1105
+ console.instance_variable_set(:@details, nil)
1106
+ console.instance_variable_set(:@model_prompt, {})
1107
+ expect(console).to receive(:handle_model_prompt).with(' ')
1108
+ console.handle(' ')
1109
+ console.instance_variable_set(:@model_prompt, nil)
1110
+ allow(console).to receive(:busy?).and_return(true)
1111
+ expect(console).to receive(:cancel)
1112
+ console.handle("\u0003")
1113
+ console.handle("\u0012")
1114
+ expect(editor.search).not_to be_nil
1115
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
1116
+ expect(game).not_to have_received(:key)
1117
+ expect([editor.text, editor.cursor]).to eq(['draft', 2])
1118
+ end
1119
+
1120
+ it 'preserves bracketed paste and the mission cursor when leaving animation focus' do
1121
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1122
+ editor = console.instance_variable_get(:@editor)
1123
+ editor.place('draft', 2)
1124
+ console.instance_variable_set(:@focus, :animation)
1125
+ [:paste_start, ' ', "\n", 'x', :paste_end].each { |key| console.handle(key) }
1126
+ expect(editor.text).to eq("dr \nxaft")
1127
+ expect(editor.cursor).to eq(5)
1128
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
1028
1129
  end
1029
1130
 
1030
1131
  it 'toggles the pane with Ctrl+T and opens swarm with Ctrl+S' do
@@ -487,9 +487,10 @@
487
487
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.wrap_untrusted_tool Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.wrap_untrusted_tool`: "}]}
488
488
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_or_read_evidenced? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_or_read_evidenced?`: "}]}
489
489
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.write_verified? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.write_verified?`: "}]}
490
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::ProviderWindow.model Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::ProviderWindow.model`: "}]}
491
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::ProviderWindow.request_model_cancel Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::ProviderWindow.request_model_cancel`: "}]}
490
492
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.new`: "}]}
491
493
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.checkpoint Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.checkpoint`: "}]}
492
- {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.model Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.model`: "}]}
493
494
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.submit Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.submit`: "}]}
494
495
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering.with_reader Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering.with_reader`: "}]}
495
496
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop::Steering::Restart.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop::Steering::Restart.new`: "}]}
@@ -1734,6 +1735,7 @@
1734
1735
  {"messages":[{"role":"user","content":"PWN::Banner.mini_asteroids Usage"},{"role":"assistant","content":"`PWN::Banner.mini_asteroids`: "}]}
1735
1736
  {"messages":[{"role":"user","content":"PWN::Banner.mini_asteroids_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_asteroids_snapshot`: "}]}
1736
1737
  {"messages":[{"role":"user","content":"PWN::Banner.mini_block_board Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_board`: "}]}
1738
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_block_plan Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_plan`: "}]}
1737
1739
  {"messages":[{"role":"user","content":"PWN::Banner.mini_cells Usage"},{"role":"assistant","content":"`PWN::Banner.mini_cells`: Supported Method Parameters\n\nReturn rows of single-column Unicode block cells, with named terminal colors. Each fresh cell has glyph, foreground and background keys; no ANSI or IO. Square means terminal-cell dimensions, not physical pixels. Caller owns borders. Use mini_frame_seconds for cadence; 1800 frames span 90 or 180 seconds. Colored blocks fill the canvas with half-cell pieces; other games cap at 16. PWN::Banner.mini_cells(\n\nname: 'optional - falling_blocks, snake, pong, or asteroids; defaults to falling_blocks',\nframe: 'optional - integer index, defaults to zero and wraps at 1800',\nwidth: 'optional - columns clamped to 0..64 for blocks, 0..16 otherwise; defaults to 16',\nheight: 'optional - rows clamped to 0..64 for blocks, 0..16 otherwise; defaults to 16',\nseed: 'optional - integer local PRNG seed, defaults to 73; retain per session',\nbranding: 'optional - include the legacy wordmark, defaults to true; false reclaims its row'\n\n)\n"}]}
1738
1740
  {"messages":[{"role":"user","content":"PWN::Banner.mini_delta Usage"},{"role":"assistant","content":"`PWN::Banner.mini_delta`: "}]}
1739
1741
  {"messages":[{"role":"user","content":"PWN::Banner.mini_emit Usage"},{"role":"assistant","content":"`PWN::Banner.mini_emit`: "}]}
@@ -1745,6 +1747,7 @@
1745
1747
  {"messages":[{"role":"user","content":"PWN::Banner.mini_landing Usage"},{"role":"assistant","content":"`PWN::Banner.mini_landing`: "}]}
1746
1748
  {"messages":[{"role":"user","content":"PWN::Banner.mini_names Usage"},{"role":"assistant","content":"`PWN::Banner.mini_names`: Supported Method Parameters\n\nList animation names; callers may sample once and retain the session choice. PWN::Banner.mini_names\n"}]}
1747
1749
  {"messages":[{"role":"user","content":"PWN::Banner.mini_pixel Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pixel`: "}]}
1750
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_play_piece Usage"},{"role":"assistant","content":"`PWN::Banner.mini_play_piece`: "}]}
1748
1751
  {"messages":[{"role":"user","content":"PWN::Banner.mini_pong Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pong`: "}]}
1749
1752
  {"messages":[{"role":"user","content":"PWN::Banner.mini_pong_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pong_snapshot`: "}]}
1750
1753
  {"messages":[{"role":"user","content":"PWN::Banner.mini_quarter_board Usage"},{"role":"assistant","content":"`PWN::Banner.mini_quarter_board`: "}]}
@@ -1786,6 +1789,13 @@
1786
1789
  {"messages":[{"role":"user","content":"PWN::Banner::Matrix.get Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.get`: Supported Method Parameters\n\nPWN::Banner::Matrix.get\n"}]}
1787
1790
  {"messages":[{"role":"user","content":"PWN::Banner::Matrix.help Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.help`: "}]}
1788
1791
  {"messages":[{"role":"user","content":"PWN::Banner::Matrix.you? Usage"},{"role":"assistant","content":"`PWN::Banner::Matrix.you?`: Supported Method Parameters\n\nPWN::Banner::Matrix.has_you\n"}]}
1792
+ {"messages":[{"role":"user","content":"PWN::Banner::MiniGame.new Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.new`: "}]}
1793
+ {"messages":[{"role":"user","content":"PWN::Banner::MiniGame.<< Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.<<`: "}]}
1794
+ {"messages":[{"role":"user","content":"PWN::Banner::MiniGame.cells Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.cells`: "}]}
1795
+ {"messages":[{"role":"user","content":"PWN::Banner::MiniGame.controls Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.controls`: "}]}
1796
+ {"messages":[{"role":"user","content":"PWN::Banner::MiniGame.key Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.key`: "}]}
1797
+ {"messages":[{"role":"user","content":"PWN::Banner::MiniGame.length Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.length`: "}]}
1798
+ {"messages":[{"role":"user","content":"PWN::Banner::MiniGame.step Usage"},{"role":"assistant","content":"`PWN::Banner::MiniGame.step`: "}]}
1789
1799
  {"messages":[{"role":"user","content":"PWN::Banner::Ninja.authors Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
1790
1800
  {"messages":[{"role":"user","content":"PWN::Banner::Ninja.get Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.get`: Supported Method Parameters\n\nPWN::Banner::Ninja.get\n"}]}
1791
1801
  {"messages":[{"role":"user","content":"PWN::Banner::Ninja.help Usage"},{"role":"assistant","content":"`PWN::Banner::Ninja.help`: "}]}
@@ -3339,6 +3349,7 @@
3339
3349
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.footer_lines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.footer_lines`: "}]}
3340
3350
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.footer_text Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.footer_text`: "}]}
3341
3351
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle`: "}]}
3352
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_animation Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_animation`: "}]}
3342
3353
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_details Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_details`: "}]}
3343
3354
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_model_prompt Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_model_prompt`: "}]}
3344
3355
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.handle_search Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.handle_search`: "}]}
@@ -3354,6 +3365,7 @@
3354
3365
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_swarm Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_swarm`: "}]}
3355
3366
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.open_system_role Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.open_system_role`: "}]}
3356
3367
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.operation_lines Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.operation_lines`: "}]}
3368
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.playable_banner Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.playable_banner`: "}]}
3357
3369
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.prepare_swarm_mission`: "}]}
3358
3370
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put`: "}]}
3359
3371
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.put_spans Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.put_spans`: "}]}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.759
4
+ version: 0.5.760
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -505,14 +505,14 @@ dependencies:
505
505
  requirements:
506
506
  - - '='
507
507
  - !ruby/object:Gem::Version
508
- version: 1.6.1
508
+ version: 1.7.0
509
509
  type: :runtime
510
510
  prerelease: false
511
511
  version_requirements: !ruby/object:Gem::Requirement
512
512
  requirements:
513
513
  - - '='
514
514
  - !ruby/object:Gem::Version
515
- version: 1.6.1
515
+ version: 1.7.0
516
516
  - !ruby/object:Gem::Dependency
517
517
  name: meshtastic
518
518
  requirement: !ruby/object:Gem::Requirement