pwn 0.5.760 → 0.5.762

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.
@@ -17,7 +17,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
17
17
  allow(PWN::AI::Agent::Loop).to receive(:may_finalize?).and_return(true)
18
18
  end
19
19
 
20
- def launch(keys, &tick)
20
+ def launch(keys, startup_delay: 0, render_delay: 0, &tick)
21
21
  require 'pty'
22
22
  require 'timeout'
23
23
  master, input = PTY.open
@@ -28,7 +28,10 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
28
28
  frame = []
29
29
  position = [0, 0]
30
30
  allow(window).to receive(:erase) { frame = [] }
31
- allow(window).to receive(:refresh) { @rendered = frame.dup }
31
+ allow(window).to receive(:refresh) do
32
+ sleep render_delay if render_delay.positive? && !keys.empty?
33
+ @rendered = frame.dup
34
+ end
32
35
  @positions = []
33
36
  allow(window).to receive(:setpos) do |*coords|
34
37
  position = coords
@@ -42,6 +45,10 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
42
45
  allow(window).to receive(:attron) { |_attribute, &block| block.call }
43
46
  screen = double('curses', init_screen: window, raw: nil, noecho: nil, curs_set: nil, close_screen: nil,
44
47
  lines: 30, cols: 110, stdscr: window, has_colors?: false, resizeterm: nil)
48
+ allow(screen).to receive(:init_screen) do
49
+ sleep startup_delay if startup_delay.positive?
50
+ window
51
+ end
45
52
  @pry = Pry.new
46
53
  @pry.config.pwn_ai_session_id = PWN::Sessions.create(title: 'console')[:id]
47
54
  @pry.config.pwn_ai = true
@@ -258,25 +265,51 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
258
265
  foreign&.join
259
266
  end
260
267
 
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
268
+ [0, 3.1].each do |startup_delay|
269
+ it "leaves a provider blocked in native IO without waiting for that read to return (startup #{startup_delay}s)" do
270
+ entered = Queue.new
271
+ returned = Queue.new
272
+ provider = nil
273
+ started = nil
274
+ allow(PWN::AI::Agent::Loop).to receive(:call_engine) do
275
+ Thread.handle_interrupt(Exception => :never) do
276
+ entered << Thread.current
277
+ sleep 8
278
+ returned << true
279
+ end
280
+ end
281
+ keys = "what color is a lemon?\n".chars
282
+ launched = Process.clock_gettime(Process::CLOCK_MONOTONIC)
283
+ launch(keys, startup_delay: startup_delay, render_delay: startup_delay.positive? ? 0.02 : 0) do
284
+ provider ||= entered.pop unless entered.empty?
285
+ if !started && provider&.status == 'sleep'
286
+ # Elapsed is painted before call_engine: it is not a provider barrier.
287
+ # Time from delivered Ctrl+C, excluding startup and per-key rendering.
288
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
289
+ "\u0003"
290
+ elsif @paint.any? { |row| row.include?('Request cancelled at a safe boundary') }
291
+ "\u0004"
292
+ else
293
+ Thread.pass
294
+ nil
295
+ end
277
296
  end
297
+ expect(started).not_to be_nil
298
+ expect(started - launched).to be >= startup_delay
299
+ expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 3
300
+ expect(returned).to be_empty
301
+ ensure
302
+ # Cancellation deliberately abandons masked IO. Release this fixture only
303
+ # after measuring return, then reap it before RSpec removes its mocks.
304
+ provider ||= entered.pop unless entered.empty?
305
+ begin
306
+ provider&.wakeup if provider&.alive?
307
+ rescue ThreadError
308
+ nil # The provider exited between alive? and wakeup.
309
+ end
310
+ provider&.join(2)
311
+ expect(provider).not_to be_alive if provider
278
312
  end
279
- expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 3
280
313
  end
281
314
 
282
315
  it 'forwards tool input, preserves completed evidence before steering, and locks settings while running' do
@@ -342,7 +375,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
342
375
  console.seed_request_history
343
376
  '/verbose '.each_char { |key| console.handle(key) }
344
377
  menu = console.instance_variable_get(:@menu)
345
- expect(menu.map { |item| item[:label] }).to eq(%w[on off])
378
+ expect(menu.map { |item| item[:label] }).to eq(%w[off on])
346
379
  draft = [editor.text.dup, editor.cursor]
347
380
  console.handle(:up)
348
381
  expect(console.instance_variable_get(:@menu_index)).to eq(1)
@@ -374,7 +407,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
374
407
  '/verbose '.each_char { |key| console.handle(key) }
375
408
  console.handle(:down)
376
409
  console.handle("\t")
377
- expect(editor.text.strip).to eq('/verbose off')
410
+ expect(editor.text.strip).to eq('/verbose on')
378
411
  console.handle("\u0015")
379
412
  '/verbose '.each_char { |key| console.handle(key) }
380
413
  console.handle(:up)
@@ -503,11 +536,11 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
503
536
 
504
537
  it 'centers capped retro artwork inside a larger cell-square canvas' do
505
538
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
506
- allow(PWN::Banner).to receive(:mini_names).and_return([:pong])
539
+ allow(PWN::Banner).to receive(:mini_names).and_return([:pacman])
507
540
  allow(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC).and_return(100.0)
508
541
  size = 24
509
542
  canvas = console.banner_frame(size)
510
- art = PWN::Banner.mini_frame(name: :pong, frame: 0, width: size, height: size, branding: false)
543
+ art = PWN::Banner.mini_frame(name: :pacman, frame: 0, width: size, height: size, branding: false)
511
544
  expect(canvas.length).to eq(size)
512
545
  expect(canvas.map(&:length)).to all(eq(size))
513
546
  top = (size - art.length) / 2
@@ -535,6 +568,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
535
568
  end
536
569
 
537
570
  it 'carves a themed cell-square banner pane only when all settings still fit, keeping bounds and draft' do
571
+ allow(PWN::Banner).to receive(:mini_names).and_return([:falling_blocks])
538
572
  PWN::Env[:ai] = { active: :ollama, tui: { theme: { category: 'magenta', header: 'white', border: 'blue', title: 'red' } },
539
573
  ollama: { model: 'fixture', system_role_content: 'Complete functional settings.', reasoning_effort: 'high' } }
540
574
  screen = double(erase: nil, refresh: nil, setpos: nil, addstr: nil)
@@ -577,7 +611,32 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
577
611
  expect([console.instance_variable_get(:@editor).text, console.instance_variable_get(:@editor).cursor]).to eq(['retained draft', 4])
578
612
  end
579
613
 
614
+ it 'keeps a full connected maze and one-cell actors in native minimum and typical console layouts' do
615
+ allow(PWN::Banner).to receive(:mini_names).and_return([:pacman])
616
+ PWN::Env[:ai] = { active: :ollama, ollama: { model: 'fixture' } }
617
+ [[100, 26], [120, 36]].each do |columns, rows|
618
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
619
+ console.instance_variable_set(:@width, columns)
620
+ console.instance_variable_set(:@height, rows)
621
+ console.instance_variable_set(:@focus, :animation)
622
+ allow(console).to receive(:put)
623
+ allow(console).to receive(:box)
624
+ allow(console).to receive(:mark_active)
625
+ expect(console.draw_header('ollama', 'fixture')).to eq(10)
626
+ game = console.instance_variable_get(:@banner_game)
627
+ frame = game.step
628
+ maze = frame[:state][:maze]
629
+ expect(maze.length).to eq(8)
630
+ expect(maze.map(&:length)).to all(eq(8))
631
+ expect(maze[2...-2].map { |row| row[2...-2] }.join.count('#')).to be >= 4
632
+ expect(game.cells(frame).flatten.count { |cell| cell[:foreground] == :red }).to eq(1)
633
+ game.key(:right)
634
+ expect(game.step[:state][:pacman]).to eq([2, 6])
635
+ end
636
+ end
637
+
580
638
  it 'grows both game canvas dimensions with the header without recursively shrinking settings' do
639
+ allow(PWN::Banner).to receive(:mini_names).and_return([:falling_blocks])
581
640
  PWN::Env[:ai] = { active: :ollama, ollama: { model: 'fixture', system_role_content: 'operator ' * 100, reasoning_effort: 'high' } }
582
641
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
583
642
  console.instance_variable_set(:@width, 120)
@@ -669,9 +728,9 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
669
728
  expect(console.instance_variable_get(:@banner_seed)).not_to eq(seed)
670
729
  end
671
730
 
672
- it 'samples Asteroids or Pong once and caches their faster cadence outside the key path' do
731
+ it 'samples Asteroids or Pac-Man once and caches the game-specific cadence outside the key path' do
673
732
  allow(PWN::Banner).to receive(:mini_cells).and_call_original
674
- %i[pong asteroids].each do |name|
733
+ %i[pacman asteroids].each do |name|
675
734
  pry = Pry.new
676
735
  pry.config.pwn_ai_session_id = 'fast-colored-session'
677
736
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: pry, input: StringIO.new, curses: nil, getch: nil)
@@ -684,7 +743,11 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
684
743
  console.banner_frame(8, cells: true)
685
744
  seed = console.instance_variable_get(:@banner_seed)
686
745
  now += 1.21
687
- expect(PWN::Banner).to receive(:mini_cells).with(name: name, frame: 24, width: 8, height: 8, seed: seed, branding: false).twice.and_call_original
746
+ if name == :pacman
747
+ expect(console).to receive(:automatic_banner).with(8, now).twice.and_call_original
748
+ else
749
+ expect(PWN::Banner).to receive(:mini_cells).with(name: name, frame: 24, width: 8, height: 8, seed: seed, branding: false).twice.and_call_original
750
+ end
688
751
  2.times { console.banner_frame(8, cells: true) }
689
752
  end
690
753
  end
@@ -1078,6 +1141,111 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1078
1141
  expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
1079
1142
  end
1080
1143
 
1144
+ it 'paints an Asteroids turn on the next draw without waiting for its physics tick' do
1145
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1146
+ console.banner_frame(8, cells: true)
1147
+ console.instance_variable_set(:@banner_name, :asteroids)
1148
+ console.instance_variable_set(:@banner_frame_seconds, 0.05)
1149
+ console.instance_variable_set(:@focus, :animation)
1150
+ first = console.playable_banner(8, 10.001)
1151
+ console.handle(:left)
1152
+ expect(console.playable_banner(8, 10.002)).not_to eq(first)
1153
+ expect(console.playable_banner(8, 10.003)).to eq(console.playable_banner(8, 10.002))
1154
+ console.instance_variable_set(:@banner_game_frame, { state: { explosion: 12 } })
1155
+ game = console.instance_variable_get(:@banner_game)
1156
+ expect(game).not_to receive(:step)
1157
+ 16.times { console.handle(:up) }
1158
+ console.playable_banner(8, 10.004)
1159
+ end
1160
+
1161
+ it 'waits for Snake input and routes timed turns across the full pane instead of restarting' do
1162
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1163
+ console.banner_frame(8, cells: true)
1164
+ console.instance_variable_set(:@banner_name, :snake)
1165
+ console.instance_variable_set(:@banner_frame_seconds, 0.1)
1166
+ console.instance_variable_set(:@focus, :animation)
1167
+ first = console.playable_banner(8, 10.0)
1168
+ 40.times { |tick| expect(console.playable_banner(8, 10.05 + (tick * 0.1))).to eq(first) }
1169
+ game = console.instance_variable_get(:@banner_game)
1170
+ frames = []
1171
+ allow(game).to(receive(:step).and_wrap_original { |method| method.call.tap { |frame| frames << frame } })
1172
+ console.handle(:right)
1173
+ console.playable_banner(8, 15.0)
1174
+ head = frames.last[:state][:body].first
1175
+ console.handle(:up)
1176
+ console.playable_banner(8, 15.05)
1177
+ expect(frames.length).to eq(1)
1178
+ console.playable_banner(8, 15.21)
1179
+ expect(frames.last[:state][:body].first).to eq([head[0], head[1] - 1])
1180
+ console.handle(:right)
1181
+ console.playable_banner(8, 15.41)
1182
+ expect(frames.last[:state][:body].first).to eq([head[0] + 1, head[1] - 1])
1183
+ expect(game.instance_variable_get(:@width)).to eq(8)
1184
+ expect(frames.last[:rows].drop(1).map(&:length)).to all(eq(16))
1185
+ expect(console.instance_variable_get(:@editor).text).to eq('')
1186
+ end
1187
+
1188
+ it 'cycles games with Ctrl+G only in animation focus and preserves swarm and modal priorities' do
1189
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1190
+ console.banner_frame(8, cells: true)
1191
+ console.instance_variable_set(:@banner_name, :falling_blocks)
1192
+ console.instance_variable_set(:@focus, :animation)
1193
+ editor = console.instance_variable_get(:@editor)
1194
+ editor.place('kept draft', 4)
1195
+ %i[snake pacman asteroids galaga frogger falling_blocks].each do |name|
1196
+ console.handle("\u0007")
1197
+ expect(console.instance_variable_get(:@banner_name)).to eq(name)
1198
+ expect(console.instance_variable_get(:@workspace)).to be_nil
1199
+ console.banner_frame(8, cells: true)
1200
+ expect(console.instance_variable_get(:@banner_game).instance_variable_get(:@name)).to eq(name.to_s)
1201
+ expect(console.footer_text).to include('^G=next game')
1202
+ expect(console.footer_text).to include('↑↓←→ start/steer · ● power') if name == :pacman
1203
+ expect(console.footer_text).to include('↑↓←→ move · Space=fire') if name == :galaga
1204
+ expect(console.footer_text).to include('↑↓←→ hop · logs → goals') if name == :frogger
1205
+ expect(console.operation_lines.join).to include('^S swarm')
1206
+ end
1207
+ console.instance_variable_set(:@details, 0)
1208
+ expect(console).to receive(:handle_details).with("\u0007")
1209
+ console.handle("\u0007")
1210
+ console.instance_variable_set(:@details, nil)
1211
+ editor.search_history
1212
+ expect(console).to receive(:handle_search).with("\u0007")
1213
+ console.handle("\u0007")
1214
+ editor.finish_search
1215
+ console.handle("\u0013")
1216
+ expect(console.instance_variable_get(:@workspace)).to be_a(Hash)
1217
+ console.handle("\u0007")
1218
+ expect(console.instance_variable_get(:@workspace)).to be_nil
1219
+ expect(console.instance_variable_get(:@banner_name)).to eq(:falling_blocks)
1220
+ console.handle("\e")
1221
+ console.handle("\u0007")
1222
+ expect(console.instance_variable_get(:@workspace)).to be_a(Hash)
1223
+ expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
1224
+ end
1225
+
1226
+ it 'delivers timed Pac-Man buffered arrows without consuming the mission draft or speeding up on repeats' do
1227
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1228
+ console.banner_frame(8, cells: true)
1229
+ console.instance_variable_set(:@banner_name, :pacman)
1230
+ console.instance_variable_set(:@banner_seed, 9)
1231
+ console.instance_variable_set(:@banner_frame_seconds, 0.1)
1232
+ console.instance_variable_set(:@focus, :animation)
1233
+ editor = console.instance_variable_get(:@editor)
1234
+ editor.place('kept draft', 4)
1235
+ console.playable_banner(7, 10.01)
1236
+ console.handle(:right)
1237
+ console.playable_banner(7, 10.11)
1238
+ expect(console.instance_variable_get(:@banner_game_frame)[:state][:pacman]).to eq([2, 5])
1239
+ console.handle(:up)
1240
+ 10.times { console.playable_banner(7, 10.12) }
1241
+ expect(console.instance_variable_get(:@banner_game_frame)[:state][:pacman]).to eq([2, 5])
1242
+ [10.21, 10.31, 10.41, 10.51, 10.61, 10.71].each { |time| console.playable_banner(7, time) }
1243
+ expect(console.instance_variable_get(:@banner_game_frame)[:state][:pacman]).to eq([3, 4])
1244
+ console.handle("\e")
1245
+ expect([editor.text, editor.cursor]).to eq(['kept draft', 4])
1246
+ expect(console.instance_variable_get(:@focus)).to eq(:mission)
1247
+ end
1248
+
1081
1249
  it 'returns to the unchanged automatic animation when focus leaves the game' do
1082
1250
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1083
1251
  allow(Process).to receive(:clock_gettime).and_return(10.0)
@@ -1092,6 +1260,28 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1092
1260
  expect(console.instance_variable_get(:@focus)).to eq(:mission)
1093
1261
  end
1094
1262
 
1263
+ it 'streams the new arcade demos on the UI owner without a synchronous full replay build' do
1264
+ %i[pacman galaga frogger].each do |name|
1265
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1266
+ allow(PWN::Banner).to receive(:mini_names).and_return([name])
1267
+ allow(Process).to receive(:clock_gettime).and_return(10.0)
1268
+ expect(PWN::Banner).not_to receive(:mini_cells)
1269
+ first = console.banner_frame(32, cells: true)
1270
+ demo = console.instance_variable_get(:@banner_demo)
1271
+ expect(demo).to be_a(PWN::Banner::MiniGame)
1272
+ expect(console.instance_variable_get(:@banner_demo_frame)[:state][:started]).to be true
1273
+ expect(first.map(&:length)).to all(eq(32))
1274
+ expect(console.banner_frame(32, cells: true)).to eq(first)
1275
+ allow(Process).to receive(:clock_gettime).and_return(10.2)
1276
+ console.banner_frame(32, cells: true)
1277
+ expect(console.instance_variable_get(:@banner_demo)).to equal(demo)
1278
+ console.instance_variable_set(:@focus, :animation)
1279
+ console.banner_frame(32, cells: true)
1280
+ expect(console.instance_variable_get(:@banner_game)).not_to equal(demo)
1281
+ expect(console.instance_variable_get(:@banner_game_frame)[:state][:started]).to be false
1282
+ end
1283
+ end
1284
+
1095
1285
  it 'keeps modal, search and cancellation priority above animation controls' do
1096
1286
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1097
1287
  console.instance_variable_set(:@focus, :animation)
@@ -1330,7 +1520,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1330
1520
  it 'keeps slash parameter completion live across spaces and clears stale menus on submit' do
1331
1521
  console = PWN::Plugins::REPL::AIConsole::Console.new(pry: Pry.new, input: StringIO.new, curses: nil, getch: nil)
1332
1522
  '/verbose '.chars.each { |key| console.handle(key) }
1333
- expect(console.instance_variable_get(:@menu).map { |item| item[:label] }).to eq(%w[on off])
1523
+ expect(console.instance_variable_get(:@menu).map { |item| item[:label] }).to eq(%w[off on])
1334
1524
  console.handle('o')
1335
1525
  console.handle('f')
1336
1526
  console.handle('f')
@@ -1488,6 +1678,7 @@ describe 'pwn-ai curses launch' do # rubocop:disable Metrics/BlockLength -- publ
1488
1678
 
1489
1679
  ["\e", "\u0003"].each do |cancel_key|
1490
1680
  it "keeps the rendered header and banner unchanged after reasoning cancellation #{cancel_key.inspect}" do
1681
+ allow(PWN::Banner).to receive(:mini_names).and_return([:falling_blocks])
1491
1682
  allow(PWN::Plugins::REPL).to receive(:persist_ai_selection).and_return(false)
1492
1683
  expect(PWN::Plugins::REPL).not_to receive(:pwn_ai_apply_model)
1493
1684
  original = Marshal.dump(PWN::Env[:ai])
@@ -5,9 +5,69 @@ require 'spec_helper'
5
5
  describe PWN::Plugins::REPL::AISwarm::Controller do
6
6
  include_context 'pwn tmp sandbox'
7
7
 
8
+ it 'routes renamed commands and missing arguments through both console entry points' do
9
+ allow(PWN::AI::Agent::Swarm).to receive(:personas).and_return(scout: { role: 'Inspect' })
10
+ allow(PWN::AI::Agent::Swarm).to receive(:create).and_return(swarm_id: 'fixture')
11
+ allow(PWN::AI::Agent::Swarm).to receive(:ask).and_return(reply: 'Direct reply')
12
+ pry = Pry.new
13
+ console = PWN::Plugins::REPL::AIConsole::Console.new(pry: pry, input: StringIO.new, curses: nil, getch: nil)
14
+ controller = described_class.new(session_id: 'fixture')
15
+ console.instance_variable_set(:@swarm, controller)
16
+ pry.config.pwn_ai_swarm = controller
17
+ ['/swarm mission', '/swarm dm', '/swarm dm scout'].each do |line|
18
+ usage = line.include?('mission') ? 'usage: mission REQUEST' : 'usage: dm NAME REQUEST'
19
+ console.submit(line)
20
+ expect(console.instance_variable_get(:@timeline).last[1]).to include(usage)
21
+ expect { PWN::Plugins::REPL.pwn_ai_dispatch_slash!(request: line, pry: pry) }.to output(/#{usage}/).to_stdout
22
+ end
23
+ expect(controller.snapshot[:jobs]).to be_empty
24
+ %w[solve roster ask].each do |old|
25
+ expect(controller.execute(line: "/swarm #{old}")[:error]).to eq("unknown /swarm command: #{old}")
26
+ end
27
+ console.submit('/swarm agents')
28
+ expect(console.instance_variable_get(:@timeline).last[1]).to include('scout')
29
+ expect { PWN::Plugins::REPL.pwn_ai_dispatch_slash!(request: '/swarm agents', pry: pry) }.to output(/scout/).to_stdout
30
+ expect { PWN::Plugins::REPL.pwn_ai_dispatch_slash!(request: '/swarm dm scout inspect', pry: pry) }.to output(/queued/).to_stdout
31
+ controller.send(:workers).each(&:join)
32
+ expect(controller.snapshot[:jobs].last).to include(command: 'dm', result: 'Direct reply')
33
+ ensure
34
+ controller&.close
35
+ end
36
+
37
+ it 'rejects a mission without an original request before starting work' do
38
+ controller = described_class.new(session_id: 'fixture')
39
+ expect(PWN::AI::Agent::Swarm).not_to receive(:create)
40
+ expect(controller.execute(line: '/swarm mission')).to include(ok: false, error: 'usage: mission REQUEST')
41
+ ensure
42
+ controller&.close
43
+ end
44
+
45
+ it 'returns visible errors for malformed commands and synchronous backend failures' do
46
+ controller = described_class.new(session_id: 'fixture')
47
+ expect(controller.execute(line: '/swarm pause absent')).to include(ok: false, error: 'job not found')
48
+ ensure
49
+ controller&.close
50
+ end
51
+
52
+ it 'preserves solve request bytes and exposes real team state and incomplete outcomes' do
53
+ request = "Write 'hello'\nthen verify --literal"
54
+ allow(PWN::AI::Agent::Swarm).to receive(:solve) do |opts|
55
+ expect(opts[:request]).to eq(request)
56
+ expect(opts[:steering]).to be_a(PWN::AI::Agent::Solve::Control)
57
+ opts[:on_state].call(roles: { protagonist: 'build', antagonist: 'done', verifier: 'waiting', integrator: 'waiting' })
58
+ { status: 'incomplete', reply: 'INCOMPLETE: missing check' }
59
+ end
60
+ controller = described_class.new(session_id: 'fixture')
61
+ controller.execute(line: "/swarm mission #{request}")
62
+ controller.send(:workers).each(&:join)
63
+ expect(controller.snapshot[:jobs].first).to include(state: 'incomplete', team: include(roles: include(protagonist: 'build')))
64
+ ensure
65
+ controller&.close
66
+ end
67
+
8
68
  it 'returns visible errors for malformed commands and synchronous backend failures' do
9
69
  controller = described_class.new(session_id: 'fixture')
10
- expect(controller.execute(line: '/swarm ask scout "unfinished')[:ok]).to be(false)
70
+ expect(controller.execute(line: '/swarm dm scout "unfinished')[:ok]).to be(false)
11
71
  allow(PWN::AI::Agent::Swarm).to receive(:create).and_raise(IOError, 'fixture storage unavailable')
12
72
  expect(controller.execute(line: '/swarm create fixture')).to include(ok: false, error: 'IOError: fixture storage unavailable')
13
73
  ensure
@@ -20,7 +80,7 @@ describe PWN::Plugins::REPL::AISwarm::Controller do
20
80
  allow(PWN::AI::Agent::Swarm).to receive(:ask).and_return(reply: 'Observed fixture result')
21
81
  controller = described_class.new(session_id: 'fixture')
22
82
  expect(controller.roster).to include(include(name: 'scout', role: 'Inspect evidence', engine: 'ollama', model: 'fixture'))
23
- queued = controller.execute(line: '/swarm ask scout inspect')
83
+ queued = controller.execute(line: '/swarm dm scout inspect')
24
84
  controller.send(:workers).each(&:join)
25
85
  expect(controller.snapshot[:jobs].first).to include(state: 'completed', result: 'Observed fixture result')
26
86
  expect(controller.execute(line: "/swarm steer #{queued[:job_id]} again")[:ok]).to be(false)
@@ -40,7 +100,7 @@ describe PWN::Plugins::REPL::AISwarm::Controller do
40
100
  { reply: 'done' }
41
101
  end
42
102
  controller = described_class.new(session_id: 'fixture')
43
- controller.execute(line: '/swarm ask scout inspect')
103
+ controller.execute(line: '/swarm dm scout inspect')
44
104
  owner, worker, local, marker = entered.pop
45
105
  expect(owner).to eq(worker)
46
106
  expect(local).to eq(:copied)
@@ -64,7 +124,7 @@ describe PWN::Plugins::REPL::AISwarm::Controller do
64
124
  controller.send(:workers).each(&:join)
65
125
  expect(calls.first[:request]).to eq('--literal mission')
66
126
  expect(calls.last[:request]).to include('reply from scout', '--literal mission')
67
- controller.execute(line: '/swarm ask broken inspect')
127
+ controller.execute(line: '/swarm dm broken inspect')
68
128
  controller.send(:workers).each(&:join)
69
129
  expect(controller.snapshot[:jobs].last).to include(state: 'failed', error: 'IOError: fixture unavailable')
70
130
  ensure
@@ -83,7 +143,7 @@ describe PWN::Plugins::REPL::AISwarm::Controller do
83
143
  opts[:steering].checkpoint(messages: [])
84
144
  end
85
145
  controller = described_class.new(session_id: 'fixture')
86
- job = controller.execute(line: '/swarm ask scout inspect')
146
+ job = controller.execute(line: '/swarm dm scout inspect')
87
147
  entered.pop
88
148
  expect(controller.execute(line: '/swarm create other')[:ok]).to be(false)
89
149
  expect(controller.execute(line: '/swarm use other')[:ok]).to be(false)
@@ -92,7 +152,7 @@ describe PWN::Plugins::REPL::AISwarm::Controller do
92
152
  expect(controller.snapshot[:jobs].first).to include(state: 'cancelled')
93
153
  expect(foreign).to be_alive
94
154
  controller.close
95
- expect(controller.execute(line: '/swarm ask scout again')[:ok]).to be(false)
155
+ expect(controller.execute(line: '/swarm dm scout again')[:ok]).to be(false)
96
156
  ensure
97
157
  controller&.close
98
158
  foreign&.kill&.join
@@ -111,7 +171,7 @@ describe PWN::Plugins::REPL::AISwarm::Controller do
111
171
  allow(PWN::AI::Agent::Swarm).to receive(:personas).and_return(scout: { role: 'scout' })
112
172
  allow(PWN::AI::Agent::Swarm).to receive(:spawn).and_return(name: 'scout')
113
173
  controller.execute(line: '/swarm spawn scout inspect evidence')
114
- queued = controller.execute(line: '/swarm ask scout inspect the fixture', on_event: ->(type, text) { events << [type, text] })
174
+ queued = controller.execute(line: '/swarm dm scout inspect the fixture', on_event: ->(type, text) { events << [type, text] })
115
175
  steering = entered.pop
116
176
  expect(queued[:ok]).to eq(true)
117
177
  expect(controller.snapshot[:jobs].first[:state]).to eq('running')
@@ -464,6 +464,7 @@
464
464
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.session_files Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.session_files`: "}]}
465
465
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.session_tool_budget Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.session_tool_budget`: "}]}
466
466
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.should_auto_introspect? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.should_auto_introspect?`: "}]}
467
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.solve_handoff? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.solve_handoff?`: "}]}
467
468
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.spill_tool_body Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.spill_tool_body`: "}]}
468
469
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.spill_tool_history! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.spill_tool_history!`: "}]}
469
470
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.squeeze_unquoted Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.squeeze_unquoted`: "}]}
@@ -855,6 +856,19 @@
855
856
  {"messages":[{"role":"user","content":"PWN::AI::Agent::SkillReview.sensitive? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::SkillReview.sensitive?`: "}]}
856
857
  {"messages":[{"role":"user","content":"PWN::AI::Agent::SkillReview.useful? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::SkillReview.useful?`: "}]}
857
858
  {"messages":[{"role":"user","content":"PWN::AI::Agent::SkillReview.verified_execution? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::SkillReview.verified_execution?`: "}]}
859
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve.authors`: "}]}
860
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve.help`: "}]}
861
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.new`: "}]}
862
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.begin_turn Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.begin_turn`: "}]}
863
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.check_stop Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.check_stop`: "}]}
864
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.checkpoint Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.checkpoint`: "}]}
865
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.guidance Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.guidance`: "}]}
866
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.model Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.model`: "}]}
867
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.pause Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.pause`: "}]}
868
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.stop Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.stop`: "}]}
869
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Control.submit Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Control.submit`: "}]}
870
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Run.new Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Run.new`: "}]}
871
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Solve::Run.run Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Solve::Run.run`: "}]}
858
872
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.ask Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.ask`: Supported Method Parameters\n\nreply = PWN::AI::Agent::Swarm.ask(\n\nname: 'required - persona name from ~/.pwn/agents.yml',\nrequest: 'required - what to ask/instruct the persona',\nswarm_id: 'optional - join an existing swarm (created if omitted)',\nto: 'optional - addressee recorded on the bus (default :all)',\non_tool: 'optional - ->(name, args, result) live-UI callback',\nsteering: 'optional - Loop::Steering for this persona turn',\nusage_observer: 'optional - callable receiving provider usage'\n\n)\n"}]}
859
873
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.authors Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.authors`: Author(s)\n\n0day Inc. <support@0dayinc.com>\n"}]}
860
874
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.broadcast Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.broadcast`: Supported Method Parameters\n\nresult = PWN::AI::Agent::Swarm.broadcast(\n\nrequest: 'required', names: 'optional - default all personas',\nswarm_id: 'optional'\n\n)\n"}]}
@@ -884,6 +898,7 @@
884
898
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.persona_session Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.persona_session`: "}]}
885
899
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.personas Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.personas`: Supported Method Parameters\n\npersonas = PWN::AI::Agent::Swarm.personas\n"}]}
886
900
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.retire Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.retire`: Supported Method Parameters\n\nPWN::AI::Agent::Swarm.retire(\n\nname: 'required - persona name',\nswarm_id: 'optional - swarm directory when local is true',\nlocal: 'optional - true retires only that swarm registry'\n\n)\n"}]}
901
+ {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.solve Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.solve`: Supported Method Parameters\n\nPWN::AI::Agent::Swarm.solve(\n\nrequest: 'required - original objective, preserved for every role',\nworkspace: 'optional - artifact root, defaults to current directory',\nrounds: 'optional - bounded repair rounds, 1..10, default 3',\nswarm_id: 'optional - existing swarm registry',\nsteering: 'optional - Solve::Control coordinator control',\non_state: 'optional - observed role state callback',\non_tool: 'optional - tool observation callback',\nusage_observer: 'optional - provider usage callback'\n\n)\n"}]}
887
902
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.spawn Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.spawn`: Supported Method Parameters\n\nPWN::AI::Agent::Swarm.spawn(\n\nname: 'required - persona name (snake_case)',\nrole: 'required - system_role_content overlay for this persona',\ntoolsets: 'optional - Array of Registry toolset names',\nengine: 'optional - :openai / :anthropic / :grok / :gemini / :ollama / :openwebui',\nmodel: 'optional - exact model identifier (defaults to selected provider model)',\nmax_iters: 'optional - per-turn iteration cap for this persona'\n\n)\n"}]}
888
903
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.specialist_roles Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.specialist_roles`: "}]}
889
904
  {"messages":[{"role":"user","content":"PWN::AI::Agent::Swarm.view_graph Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Swarm.view_graph`: "}]}
@@ -1736,16 +1751,33 @@
1736
1751
  {"messages":[{"role":"user","content":"PWN::Banner.mini_asteroids_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_asteroids_snapshot`: "}]}
1737
1752
  {"messages":[{"role":"user","content":"PWN::Banner.mini_block_board Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_board`: "}]}
1738
1753
  {"messages":[{"role":"user","content":"PWN::Banner.mini_block_plan Usage"},{"role":"assistant","content":"`PWN::Banner.mini_block_plan`: "}]}
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"}]}
1754
+ {"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. Blocks, Pac-Man, Galaga and Frogger fill up to 64 cells; other games cap at 16. PWN::Banner.mini_cells(\n\nname: 'optional - falling_blocks, snake, pacman, asteroids, galaga or frogger; legacy pong accepted; defaults to falling_blocks',\nframe: 'optional - integer index, defaults to zero and wraps at 1800',\nwidth: 'optional - columns clamped to 0..64 for blocks/Pac-Man/Galaga/Frogger, 0..16 otherwise; defaults to 16',\nheight: 'optional - rows clamped to 0..64 for blocks/Pac-Man/Galaga/Frogger, 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"}]}
1740
1755
  {"messages":[{"role":"user","content":"PWN::Banner.mini_delta Usage"},{"role":"assistant","content":"`PWN::Banner.mini_delta`: "}]}
1756
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_dot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_dot`: "}]}
1741
1757
  {"messages":[{"role":"user","content":"PWN::Banner.mini_emit Usage"},{"role":"assistant","content":"`PWN::Banner.mini_emit`: "}]}
1742
1758
  {"messages":[{"role":"user","content":"PWN::Banner.mini_falling_blocks Usage"},{"role":"assistant","content":"`PWN::Banner.mini_falling_blocks`: "}]}
1743
1759
  {"messages":[{"role":"user","content":"PWN::Banner.mini_fits? Usage"},{"role":"assistant","content":"`PWN::Banner.mini_fits?`: "}]}
1744
- {"messages":[{"role":"user","content":"PWN::Banner.mini_frame Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frame`: Supported Method Parameters\n\nReturn fresh ASCII rows for decorative retro loops, not interactive games. Use equal width and height for a square; 5..16 retains the complete artwork. Smaller panes degrade to a wordmark. Caller owns randomness, clock and IO. PWN::Banner.mini_frame(\n\nname: 'optional - falling_blocks, snake, pong, or asteroids; defaults to falling_blocks',\nframe: 'optional - integer frame index, defaults to zero and wraps at 60',\nwidth: 'optional - output columns clamped to zero through 16, defaults to 16',\nheight: 'optional - output rows clamped to zero through 16, defaults to 16',\nbranding: 'optional - include the legacy wordmark, defaults to true; false reclaims its row'\n\n)\n"}]}
1760
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_frame Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frame`: Supported Method Parameters\n\nReturn fresh ASCII rows for decorative retro loops, not interactive games. Use equal width and height for a square; 5..16 retains the complete artwork. Smaller panes degrade to a wordmark. Caller owns randomness, clock and IO. PWN::Banner.mini_frame(\n\nname: 'optional - falling_blocks, snake, pacman, asteroids, galaga or frogger; legacy pong accepted; defaults to falling_blocks',\nframe: 'optional - integer frame index, defaults to zero and wraps at 60',\nwidth: 'optional - output columns clamped to zero through 16, defaults to 16',\nheight: 'optional - output rows clamped to zero through 16, defaults to 16',\nbranding: 'optional - include the legacy wordmark, defaults to true; false reclaims its row'\n\n)\n"}]}
1745
1761
  {"messages":[{"role":"user","content":"PWN::Banner.mini_frame_seconds Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frame_seconds`: Supported Method Parameters\n\nReturn the presentation interval; simulations preserve their gameplay speed. PWN::Banner.mini_frame_seconds(\n\nname: 'optional - animation name, defaults to falling_blocks'\n\n)\n"}]}
1746
1762
  {"messages":[{"role":"user","content":"PWN::Banner.mini_freeze Usage"},{"role":"assistant","content":"`PWN::Banner.mini_freeze`: "}]}
1763
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_frogger_contact Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frogger_contact`: "}]}
1764
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_frogger_demo Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frogger_demo`: "}]}
1765
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_frogger_move Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frogger_move`: "}]}
1766
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_frogger_round Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frogger_round`: "}]}
1767
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_frogger_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_frogger_snapshot`: "}]}
1768
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_galaga_move Usage"},{"role":"assistant","content":"`PWN::Banner.mini_galaga_move`: "}]}
1769
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_galaga_round Usage"},{"role":"assistant","content":"`PWN::Banner.mini_galaga_round`: "}]}
1770
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_galaga_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_galaga_snapshot`: "}]}
1747
1771
  {"messages":[{"role":"user","content":"PWN::Banner.mini_landing Usage"},{"role":"assistant","content":"`PWN::Banner.mini_landing`: "}]}
1748
1772
  {"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"}]}
1773
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman`: "}]}
1774
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman_contact Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman_contact`: "}]}
1775
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman_ghosts Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman_ghosts`: "}]}
1776
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman_move Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman_move`: "}]}
1777
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman_neighbors Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman_neighbors`: "}]}
1778
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman_round Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman_round`: "}]}
1779
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman_route Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman_route`: "}]}
1780
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_pacman_snapshot Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pacman_snapshot`: "}]}
1749
1781
  {"messages":[{"role":"user","content":"PWN::Banner.mini_pixel Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pixel`: "}]}
1750
1782
  {"messages":[{"role":"user","content":"PWN::Banner.mini_play_piece Usage"},{"role":"assistant","content":"`PWN::Banner.mini_play_piece`: "}]}
1751
1783
  {"messages":[{"role":"user","content":"PWN::Banner.mini_pong Usage"},{"role":"assistant","content":"`PWN::Banner.mini_pong`: "}]}
@@ -1756,6 +1788,9 @@
1756
1788
  {"messages":[{"role":"user","content":"PWN::Banner.mini_rotations Usage"},{"role":"assistant","content":"`PWN::Banner.mini_rotations`: "}]}
1757
1789
  {"messages":[{"role":"user","content":"PWN::Banner.mini_simulate_asteroids Usage"},{"role":"assistant","content":"`PWN::Banner.mini_simulate_asteroids`: "}]}
1758
1790
  {"messages":[{"role":"user","content":"PWN::Banner.mini_simulate_falling_blocks Usage"},{"role":"assistant","content":"`PWN::Banner.mini_simulate_falling_blocks`: "}]}
1791
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_simulate_frogger Usage"},{"role":"assistant","content":"`PWN::Banner.mini_simulate_frogger`: "}]}
1792
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_simulate_galaga Usage"},{"role":"assistant","content":"`PWN::Banner.mini_simulate_galaga`: "}]}
1793
+ {"messages":[{"role":"user","content":"PWN::Banner.mini_simulate_pacman Usage"},{"role":"assistant","content":"`PWN::Banner.mini_simulate_pacman`: "}]}
1759
1794
  {"messages":[{"role":"user","content":"PWN::Banner.mini_simulate_pong Usage"},{"role":"assistant","content":"`PWN::Banner.mini_simulate_pong`: "}]}
1760
1795
  {"messages":[{"role":"user","content":"PWN::Banner.mini_simulate_snake Usage"},{"role":"assistant","content":"`PWN::Banner.mini_simulate_snake`: "}]}
1761
1796
  {"messages":[{"role":"user","content":"PWN::Banner.mini_snake Usage"},{"role":"assistant","content":"`PWN::Banner.mini_snake`: "}]}
@@ -3317,6 +3352,7 @@
3317
3352
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.new Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.new`: "}]}
3318
3353
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.accept_completion Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.accept_completion`: "}]}
3319
3354
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.add Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.add`: "}]}
3355
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.automatic_banner Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.automatic_banner`: "}]}
3320
3356
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.banner_frame Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.banner_frame`: "}]}
3321
3357
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.begin_request_metrics Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.begin_request_metrics`: "}]}
3322
3358
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.bounded_event Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.bounded_event`: "}]}
@@ -3379,6 +3415,7 @@
3379
3415
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.seed_request_history Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.seed_request_history`: "}]}
3380
3416
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.send_tool_input Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.send_tool_input`: "}]}
3381
3417
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.setup_colors Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.setup_colors`: "}]}
3418
+ {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.solve_rows Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.solve_rows`: "}]}
3382
3419
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.start_request Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.start_request`: "}]}
3383
3420
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.state Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.state`: "}]}
3384
3421
  {"messages":[{"role":"user","content":"PWN::Plugins::REPL::AIConsole::Console.stop_worker Usage"},{"role":"assistant","content":"`PWN::Plugins::REPL::AIConsole::Console.stop_worker`: "}]}