aidp 0.9.6 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/aidp/analyze/error_handler.rb +4 -2
- data/lib/aidp/{analysis → analyze}/kb_inspector.rb +106 -89
- data/lib/aidp/analyze/prioritizer.rb +3 -2
- data/lib/aidp/analyze/ruby_maat_integration.rb +20 -3
- data/lib/aidp/analyze/runner.rb +27 -9
- data/lib/aidp/{analysis → analyze}/seams.rb +1 -1
- data/lib/aidp/analyze/steps.rb +7 -7
- data/lib/aidp/{analysis → analyze}/tree_sitter_grammar_loader.rb +22 -5
- data/lib/aidp/{analysis → analyze}/tree_sitter_scan.rb +32 -15
- data/lib/aidp/cli/first_run_wizard.rb +37 -28
- data/lib/aidp/cli/jobs_command.rb +37 -18
- data/lib/aidp/cli/terminal_io.rb +3 -3
- data/lib/aidp/cli.rb +131 -63
- data/lib/aidp/execute/runner.rb +27 -9
- data/lib/aidp/execute/steps.rb +18 -18
- data/lib/aidp/execute/workflow_selector.rb +36 -21
- data/lib/aidp/harness/enhanced_runner.rb +3 -3
- data/lib/aidp/harness/provider_factory.rb +3 -1
- data/lib/aidp/harness/provider_manager.rb +34 -15
- data/lib/aidp/harness/runner.rb +24 -5
- data/lib/aidp/harness/simple_user_interface.rb +19 -4
- data/lib/aidp/harness/status_display.rb +121 -104
- data/lib/aidp/harness/ui/enhanced_tui.rb +5 -5
- data/lib/aidp/harness/ui/error_handler.rb +3 -2
- data/lib/aidp/harness/ui/frame_manager.rb +52 -32
- data/lib/aidp/harness/ui/navigation/main_menu.rb +23 -14
- data/lib/aidp/harness/ui/progress_display.rb +28 -5
- data/lib/aidp/harness/ui/status_widget.rb +17 -8
- data/lib/aidp/harness/ui/workflow_controller.rb +25 -9
- data/lib/aidp/harness/user_interface.rb +341 -328
- data/lib/aidp/provider_manager.rb +10 -6
- data/lib/aidp/providers/anthropic.rb +3 -3
- data/lib/aidp/providers/base.rb +20 -1
- data/lib/aidp/providers/cursor.rb +6 -8
- data/lib/aidp/providers/gemini.rb +3 -3
- data/lib/aidp/providers/github_copilot.rb +264 -0
- data/lib/aidp/providers/opencode.rb +6 -8
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +4 -4
- metadata +6 -6
- data/lib/aidp/analyze/progress_visualizer.rb +0 -314
data/lib/aidp/analyze/steps.rb
CHANGED
@@ -5,37 +5,37 @@ module Aidp
|
|
5
5
|
module Steps
|
6
6
|
SPEC = {
|
7
7
|
"01_REPOSITORY_ANALYSIS" => {
|
8
|
-
"templates" => ["
|
8
|
+
"templates" => ["01_REPOSITORY_ANALYSIS.md"],
|
9
9
|
"description" => "Initial code-maat based repository mining",
|
10
10
|
"outs" => ["docs/analysis/repository_analysis.md"],
|
11
11
|
"gate" => false
|
12
12
|
},
|
13
13
|
"02_ARCHITECTURE_ANALYSIS" => {
|
14
|
-
"templates" => ["
|
14
|
+
"templates" => ["02_ARCHITECTURE_ANALYSIS.md"],
|
15
15
|
"description" => "Identify architectural patterns, dependencies, and violations",
|
16
16
|
"outs" => ["docs/analysis/architecture_analysis.md"],
|
17
17
|
"gate" => true
|
18
18
|
},
|
19
19
|
"03_TEST_ANALYSIS" => {
|
20
|
-
"templates" => ["
|
20
|
+
"templates" => ["03_TEST_ANALYSIS.md"],
|
21
21
|
"description" => "Analyze existing test coverage and identify gaps",
|
22
22
|
"outs" => ["docs/analysis/test_analysis.md"],
|
23
23
|
"gate" => false
|
24
24
|
},
|
25
25
|
"04_FUNCTIONALITY_ANALYSIS" => {
|
26
|
-
"templates" => ["
|
26
|
+
"templates" => ["04_FUNCTIONALITY_ANALYSIS.md"],
|
27
27
|
"description" => "Map features, identify dead code, analyze complexity",
|
28
28
|
"outs" => ["docs/analysis/functionality_analysis.md"],
|
29
29
|
"gate" => false
|
30
30
|
},
|
31
31
|
"05_DOCUMENTATION_ANALYSIS" => {
|
32
|
-
"templates" => ["
|
32
|
+
"templates" => ["05_DOCUMENTATION_ANALYSIS.md"],
|
33
33
|
"description" => "Identify missing documentation and generate what's needed",
|
34
34
|
"outs" => ["docs/analysis/documentation_analysis.md"],
|
35
35
|
"gate" => false
|
36
36
|
},
|
37
37
|
"06_STATIC_ANALYSIS" => {
|
38
|
-
"templates" => ["
|
38
|
+
"templates" => ["06_STATIC_ANALYSIS.md"],
|
39
39
|
"description" => "Check for existing tools and recommend improvements",
|
40
40
|
"outs" => ["docs/analysis/static_analysis.md"],
|
41
41
|
"gate" => false
|
@@ -47,7 +47,7 @@ module Aidp
|
|
47
47
|
"gate" => false
|
48
48
|
},
|
49
49
|
"07_REFACTORING_RECOMMENDATIONS" => {
|
50
|
-
"templates" => ["
|
50
|
+
"templates" => ["07_REFACTORING_RECOMMENDATIONS.md"],
|
51
51
|
"description" => "Provide actionable refactoring guidance",
|
52
52
|
"outs" => ["docs/analysis/refactoring_recommendations.md"],
|
53
53
|
"gate" => true
|
@@ -2,10 +2,11 @@
|
|
2
2
|
|
3
3
|
require "pathname"
|
4
4
|
require "tree_sitter"
|
5
|
+
require "tty-prompt"
|
5
6
|
require "fileutils"
|
6
7
|
|
7
8
|
module Aidp
|
8
|
-
module
|
9
|
+
module Analyze
|
9
10
|
class TreeSitterGrammarLoader
|
10
11
|
# Default grammar configurations
|
11
12
|
GRAMMAR_CONFIGS = {
|
@@ -61,10 +62,11 @@ module Aidp
|
|
61
62
|
}
|
62
63
|
}.freeze
|
63
64
|
|
64
|
-
def initialize(project_dir = Dir.pwd)
|
65
|
+
def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new)
|
65
66
|
@project_dir = project_dir
|
66
67
|
@grammars_dir = File.join(project_dir, ".aidp", "grammars")
|
67
68
|
@loaded_grammars = {}
|
69
|
+
@prompt = prompt
|
68
70
|
end
|
69
71
|
|
70
72
|
# Load grammar for a specific language
|
@@ -92,7 +94,7 @@ module Aidp
|
|
92
94
|
grammar_path = File.join(@grammars_dir, language)
|
93
95
|
|
94
96
|
unless File.exist?(grammar_path)
|
95
|
-
|
97
|
+
display_message("Installing Tree-sitter grammar for #{language}...", type: :info)
|
96
98
|
install_grammar(language, config)
|
97
99
|
end
|
98
100
|
end
|
@@ -110,7 +112,7 @@ module Aidp
|
|
110
112
|
require "json"
|
111
113
|
File.write(File.join(grammar_path, "grammar.json"), JSON.generate(config))
|
112
114
|
|
113
|
-
|
115
|
+
display_message("Grammar for #{language} marked as available", type: :success)
|
114
116
|
end
|
115
117
|
|
116
118
|
def create_parser(language, config)
|
@@ -140,7 +142,7 @@ module Aidp
|
|
140
142
|
real: true
|
141
143
|
}
|
142
144
|
rescue TreeSitter::ParserNotFoundError => e
|
143
|
-
|
145
|
+
display_message("Warning: Tree-sitter parser not found for #{language}: #{e.message}", type: :warn)
|
144
146
|
create_mock_parser(language)
|
145
147
|
end
|
146
148
|
|
@@ -476,6 +478,21 @@ module Aidp
|
|
476
478
|
|
477
479
|
nodes
|
478
480
|
end
|
481
|
+
|
482
|
+
private
|
483
|
+
|
484
|
+
# Helper method for consistent message display using TTY::Prompt
|
485
|
+
def display_message(message, type: :info)
|
486
|
+
color = case type
|
487
|
+
when :error then :red
|
488
|
+
when :warn then :yellow
|
489
|
+
when :success then :green
|
490
|
+
when :highlight then :cyan
|
491
|
+
else :white
|
492
|
+
end
|
493
|
+
|
494
|
+
@prompt.say(message, color: color)
|
495
|
+
end
|
479
496
|
end
|
480
497
|
end
|
481
498
|
end
|
@@ -4,19 +4,21 @@ require "json"
|
|
4
4
|
require "fileutils"
|
5
5
|
require "digest"
|
6
6
|
require "etc"
|
7
|
+
require "tty-prompt"
|
7
8
|
|
8
9
|
require_relative "tree_sitter_grammar_loader"
|
9
10
|
require_relative "seams"
|
10
11
|
|
11
12
|
module Aidp
|
12
|
-
module
|
13
|
+
module Analyze
|
13
14
|
class TreeSitterScan
|
14
|
-
def initialize(root: Dir.pwd, kb_dir: ".aidp/kb", langs: %w[ruby], threads: Etc.nprocessors)
|
15
|
+
def initialize(root: Dir.pwd, kb_dir: ".aidp/kb", langs: %w[ruby], threads: Etc.nprocessors, prompt: TTY::Prompt.new)
|
15
16
|
@root = File.expand_path(root)
|
16
17
|
@kb_dir = File.expand_path(kb_dir, @root)
|
17
18
|
@langs = Array(langs)
|
18
19
|
@threads = threads
|
19
|
-
@
|
20
|
+
@prompt = prompt
|
21
|
+
@grammar_loader = TreeSitterGrammarLoader.new(@root, prompt: @prompt)
|
20
22
|
|
21
23
|
# Data structures to accumulate analysis results
|
22
24
|
@symbols = []
|
@@ -34,14 +36,14 @@ module Aidp
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def run
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
display_message("🔍 Starting Tree-sitter static analysis...", type: :highlight)
|
40
|
+
display_message("📁 Root: #{@root}", type: :info)
|
41
|
+
display_message("🗂️ KB Directory: #{@kb_dir}", type: :info)
|
42
|
+
display_message("🌐 Languages: #{@langs.join(", ")}", type: :info)
|
43
|
+
display_message("🧵 Threads: #{@threads}", type: :info)
|
42
44
|
|
43
45
|
files = discover_files
|
44
|
-
|
46
|
+
display_message("📄 Found #{files.length} files to analyze", type: :info)
|
45
47
|
|
46
48
|
prepare_kb_dir
|
47
49
|
load_cache
|
@@ -49,8 +51,8 @@ module Aidp
|
|
49
51
|
parallel_parse(files)
|
50
52
|
write_kb_files
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
+
display_message("✅ Tree-sitter analysis complete!", type: :success)
|
55
|
+
display_message("📊 Generated KB files in #{@kb_dir}", type: :success)
|
54
56
|
end
|
55
57
|
|
56
58
|
private
|
@@ -144,14 +146,14 @@ module Aidp
|
|
144
146
|
end
|
145
147
|
|
146
148
|
def parallel_parse(files)
|
147
|
-
|
149
|
+
display_message("🔄 Parsing files in parallel...", type: :info)
|
148
150
|
|
149
151
|
# Group files by language for efficient processing
|
150
152
|
files_by_lang = files.group_by { |file| detect_language(file) }
|
151
153
|
|
152
154
|
# Process each language group
|
153
155
|
files_by_lang.each do |lang, lang_files|
|
154
|
-
|
156
|
+
display_message("📝 Processing #{lang_files.length} #{lang} files...", type: :info)
|
155
157
|
|
156
158
|
# Load grammar for this language
|
157
159
|
grammar = @grammar_loader.load_grammar(lang)
|
@@ -614,7 +616,7 @@ module Aidp
|
|
614
616
|
end
|
615
617
|
|
616
618
|
def write_kb_files
|
617
|
-
|
619
|
+
display_message("💾 Writing knowledge base files...", type: :info)
|
618
620
|
|
619
621
|
prepare_kb_dir
|
620
622
|
|
@@ -637,7 +639,7 @@ module Aidp
|
|
637
639
|
def write_json_file(filename, data)
|
638
640
|
file_path = File.join(@kb_dir, filename)
|
639
641
|
File.write(file_path, JSON.pretty_generate(data))
|
640
|
-
|
642
|
+
display_message("📄 Written #{filename} (#{data.length} entries)", type: :success)
|
641
643
|
end
|
642
644
|
|
643
645
|
def generate_hotspots
|
@@ -681,6 +683,21 @@ module Aidp
|
|
681
683
|
# This would analyze test file naming and content
|
682
684
|
[]
|
683
685
|
end
|
686
|
+
|
687
|
+
private
|
688
|
+
|
689
|
+
# Helper method for consistent message display using TTY::Prompt
|
690
|
+
def display_message(message, type: :info)
|
691
|
+
color = case type
|
692
|
+
when :error then :red
|
693
|
+
when :warn then :yellow
|
694
|
+
when :success then :green
|
695
|
+
when :highlight then :cyan
|
696
|
+
else :white
|
697
|
+
end
|
698
|
+
|
699
|
+
@prompt.say(message, color: color)
|
700
|
+
end
|
684
701
|
end
|
685
702
|
end
|
686
703
|
end
|
@@ -10,40 +10,53 @@ module Aidp
|
|
10
10
|
class FirstRunWizard
|
11
11
|
TEMPLATES_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "templates"))
|
12
12
|
|
13
|
-
def self.ensure_config(project_dir,
|
13
|
+
def self.ensure_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
|
14
14
|
return true if Aidp::Config.config_exists?(project_dir)
|
15
15
|
|
16
|
-
wizard = new(project_dir,
|
16
|
+
wizard = new(project_dir, prompt: prompt)
|
17
17
|
|
18
|
-
if non_interactive
|
18
|
+
if non_interactive
|
19
19
|
# Non-interactive environment - create minimal config silently
|
20
20
|
path = wizard.send(:write_minimal_config, project_dir)
|
21
|
-
|
21
|
+
wizard.send(:display_message, "Created minimal configuration at #{wizard.send(:relative, path)} (non-interactive default)", type: :success)
|
22
22
|
return true
|
23
23
|
end
|
24
24
|
|
25
25
|
wizard.run
|
26
26
|
end
|
27
27
|
|
28
|
-
def self.setup_config(project_dir,
|
29
|
-
wizard = new(project_dir,
|
28
|
+
def self.setup_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
|
29
|
+
wizard = new(project_dir, prompt: prompt)
|
30
30
|
|
31
|
-
if non_interactive
|
31
|
+
if non_interactive
|
32
32
|
# Non-interactive environment - skip setup
|
33
|
-
|
33
|
+
wizard.send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
|
34
34
|
return true
|
35
35
|
end
|
36
36
|
|
37
37
|
wizard.run_setup_config
|
38
38
|
end
|
39
39
|
|
40
|
-
def initialize(project_dir,
|
40
|
+
def initialize(project_dir, prompt: TTY::Prompt.new)
|
41
41
|
@project_dir = project_dir
|
42
|
-
@input = input
|
43
|
-
@output = output
|
44
42
|
@prompt = prompt
|
45
43
|
end
|
46
44
|
|
45
|
+
# Helper method for consistent message display using TTY::Prompt
|
46
|
+
def display_message(message, type: :info)
|
47
|
+
color = case type
|
48
|
+
when :error then :red
|
49
|
+
when :success then :green
|
50
|
+
when :warning then :yellow
|
51
|
+
when :info then :blue
|
52
|
+
when :highlight then :cyan
|
53
|
+
when :muted then :bright_black
|
54
|
+
else :white
|
55
|
+
end
|
56
|
+
|
57
|
+
@prompt.say(message, color: color)
|
58
|
+
end
|
59
|
+
|
47
60
|
def run
|
48
61
|
banner
|
49
62
|
loop do
|
@@ -51,10 +64,10 @@ module Aidp
|
|
51
64
|
case choice
|
52
65
|
when "1" then return finish(write_quick_config(@project_dir))
|
53
66
|
when "2" then return finish(run_custom)
|
54
|
-
when "q", "Q" then
|
67
|
+
when "q", "Q" then display_message("Exiting without creating configuration.")
|
55
68
|
return false
|
56
69
|
else
|
57
|
-
|
70
|
+
display_message("Invalid selection. Please choose one of the listed options.", type: :warning)
|
58
71
|
end
|
59
72
|
end
|
60
73
|
end
|
@@ -81,14 +94,14 @@ module Aidp
|
|
81
94
|
private
|
82
95
|
|
83
96
|
def banner
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
97
|
+
display_message("\n🚀 First-time setup detected", type: :highlight)
|
98
|
+
display_message("No 'aidp.yml' configuration file found in #{relative(@project_dir)}.")
|
99
|
+
display_message("Let's create one so you can start using AI Dev Pipeline.")
|
100
|
+
display_message("")
|
88
101
|
end
|
89
102
|
|
90
103
|
def ask_choice
|
91
|
-
|
104
|
+
display_message("Choose a configuration style:") unless @asking
|
92
105
|
|
93
106
|
options = {
|
94
107
|
"Quick setup (cursor + macos, no API keys needed)" => "1",
|
@@ -101,11 +114,11 @@ module Aidp
|
|
101
114
|
|
102
115
|
def finish(path)
|
103
116
|
if path
|
104
|
-
|
105
|
-
|
117
|
+
display_message("\n✅ Configuration created at #{relative(path)}", type: :success)
|
118
|
+
display_message("You can edit this file anytime. Continuing startup...\n")
|
106
119
|
true
|
107
120
|
else
|
108
|
-
|
121
|
+
display_message("❌ Failed to create configuration file.", type: :error)
|
109
122
|
false
|
110
123
|
end
|
111
124
|
end
|
@@ -113,7 +126,7 @@ module Aidp
|
|
113
126
|
def copy_template(filename)
|
114
127
|
src = File.join(TEMPLATES_DIR, filename)
|
115
128
|
unless File.exist?(src)
|
116
|
-
|
129
|
+
display_message("Template not found: #{filename}", type: :error)
|
117
130
|
return nil
|
118
131
|
end
|
119
132
|
dest = File.join(@project_dir, "aidp.yml")
|
@@ -310,14 +323,10 @@ module Aidp
|
|
310
323
|
|
311
324
|
def ask(prompt, default: nil)
|
312
325
|
if default
|
313
|
-
@
|
326
|
+
@prompt.ask("#{prompt}:", default: default)
|
314
327
|
else
|
315
|
-
@
|
328
|
+
@prompt.ask("#{prompt}:")
|
316
329
|
end
|
317
|
-
@output.flush
|
318
|
-
ans = @input.gets&.strip
|
319
|
-
return default if (ans.nil? || ans.empty?) && default
|
320
|
-
ans
|
321
330
|
end
|
322
331
|
|
323
332
|
def relative(path)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "tty-prompt"
|
3
4
|
require "tty-box"
|
4
5
|
require "pastel"
|
5
6
|
require "io/console"
|
@@ -10,8 +11,9 @@ require_relative "../storage/file_manager"
|
|
10
11
|
module Aidp
|
11
12
|
class CLI
|
12
13
|
class JobsCommand
|
13
|
-
def initialize(input:
|
14
|
-
@io = TerminalIO.new(input, output)
|
14
|
+
def initialize(input: nil, output: nil, prompt: TTY::Prompt.new)
|
15
|
+
@io = TerminalIO.new(input: input, output: output)
|
16
|
+
@prompt = prompt
|
15
17
|
@pastel = Pastel.new
|
16
18
|
@running = true
|
17
19
|
@view_mode = :list
|
@@ -21,18 +23,35 @@ module Aidp
|
|
21
23
|
@screen_width = 80 # Default screen width
|
22
24
|
end
|
23
25
|
|
26
|
+
private
|
27
|
+
|
28
|
+
def display_message(message, type: :info)
|
29
|
+
color = case type
|
30
|
+
when :error then :red
|
31
|
+
when :success then :green
|
32
|
+
when :warning then :yellow
|
33
|
+
when :info then :blue
|
34
|
+
when :highlight then :cyan
|
35
|
+
when :muted then :bright_black
|
36
|
+
else :white
|
37
|
+
end
|
38
|
+
@prompt.say(message, color: color)
|
39
|
+
end
|
40
|
+
|
41
|
+
public
|
42
|
+
|
24
43
|
def run
|
25
44
|
# Simple harness jobs display
|
26
45
|
jobs = fetch_harness_jobs
|
27
46
|
|
28
47
|
if jobs.empty?
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
48
|
+
display_message("Harness Jobs", type: :info)
|
49
|
+
display_message("-" * @screen_width, type: :muted)
|
50
|
+
display_message("")
|
51
|
+
display_message("No harness jobs found", type: :info)
|
52
|
+
display_message("")
|
53
|
+
display_message("Harness jobs are background tasks that run during harness mode.", type: :info)
|
54
|
+
display_message("They are stored as JSON files in the .aidp/harness_logs/ directory.", type: :info)
|
36
55
|
else
|
37
56
|
render_harness_jobs(jobs)
|
38
57
|
end
|
@@ -64,7 +83,7 @@ module Aidp
|
|
64
83
|
|
65
84
|
jobs << job_info
|
66
85
|
rescue JSON::ParserError => e
|
67
|
-
|
86
|
+
display_message("Warning: Could not parse harness log #{log_file}: #{e.message}", type: :warning) if ENV["AIDP_DEBUG"]
|
68
87
|
end
|
69
88
|
|
70
89
|
# Sort by creation time (newest first)
|
@@ -91,9 +110,9 @@ module Aidp
|
|
91
110
|
|
92
111
|
# Render harness jobs in a simple table
|
93
112
|
def render_harness_jobs(jobs)
|
94
|
-
|
95
|
-
|
96
|
-
|
113
|
+
display_message("Harness Jobs", type: :info)
|
114
|
+
display_message("-" * @screen_width, type: :muted)
|
115
|
+
display_message("")
|
97
116
|
|
98
117
|
# Create job content for TTY::Box
|
99
118
|
job_content = []
|
@@ -121,12 +140,12 @@ module Aidp
|
|
121
140
|
border: :thick,
|
122
141
|
padding: [1, 2]
|
123
142
|
)
|
124
|
-
|
143
|
+
display_message(box)
|
125
144
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
145
|
+
display_message("")
|
146
|
+
display_message("Total: #{jobs.length} harness job(s)", type: :info)
|
147
|
+
display_message("")
|
148
|
+
display_message("Note: Harness jobs are stored as JSON files in .aidp/harness_logs/", type: :muted)
|
130
149
|
end
|
131
150
|
|
132
151
|
# Format timestamp for display
|
data/lib/aidp/cli/terminal_io.rb
CHANGED
@@ -5,9 +5,9 @@ require "stringio"
|
|
5
5
|
module Aidp
|
6
6
|
class CLI
|
7
7
|
class TerminalIO
|
8
|
-
def initialize(input
|
9
|
-
@input = input
|
10
|
-
@output = output
|
8
|
+
def initialize(input: nil, output: nil)
|
9
|
+
@input = input || $stdin
|
10
|
+
@output = output || $stdout
|
11
11
|
end
|
12
12
|
|
13
13
|
def ready?
|