aidp 0.14.1 ā 0.15.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/README.md +46 -45
- data/lib/aidp/cli.rb +46 -38
- data/lib/aidp/debug_mixin.rb +34 -33
- data/lib/aidp/execute/agent_signal_parser.rb +46 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +5 -0
- data/lib/aidp/execute/checkpoint.rb +28 -5
- data/lib/aidp/execute/deterministic_unit.rb +254 -0
- data/lib/aidp/execute/interactive_repl.rb +7 -0
- data/lib/aidp/execute/steps.rb +1 -1
- data/lib/aidp/execute/work_loop_runner.rb +187 -30
- data/lib/aidp/execute/work_loop_unit_scheduler.rb +190 -0
- data/lib/aidp/harness/config_schema.rb +91 -1
- data/lib/aidp/harness/configuration.rb +60 -1
- data/lib/aidp/harness/enhanced_runner.rb +2 -0
- data/lib/aidp/harness/provider_info.rb +14 -4
- data/lib/aidp/harness/provider_manager.rb +64 -12
- data/lib/aidp/jobs/background_runner.rb +10 -3
- data/lib/aidp/logger.rb +10 -71
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/providers/github_copilot.rb +12 -0
- data/lib/aidp/rescue_logging.rb +36 -0
- data/lib/aidp/setup/wizard.rb +42 -46
- data/lib/aidp/storage/csv_storage.rb +33 -7
- data/lib/aidp/storage/json_storage.rb +33 -10
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +95 -22
- data/templates/implementation/simple_task.md +5 -0
- metadata +5 -2
- data/lib/aidp/debug_logger.rb +0 -195
|
@@ -5,6 +5,7 @@ require_relative "../harness/provider_factory"
|
|
|
5
5
|
require_relative "../harness/config_manager"
|
|
6
6
|
require_relative "definitions"
|
|
7
7
|
require_relative "../message_display"
|
|
8
|
+
require_relative "../debug_mixin"
|
|
8
9
|
require_relative "../cli/enhanced_input"
|
|
9
10
|
|
|
10
11
|
module Aidp
|
|
@@ -13,6 +14,7 @@ module Aidp
|
|
|
13
14
|
# Acts as a copilot to match user intent to AIDP capabilities
|
|
14
15
|
class GuidedAgent
|
|
15
16
|
include Aidp::MessageDisplay
|
|
17
|
+
include Aidp::DebugMixin
|
|
16
18
|
|
|
17
19
|
class ConversationError < StandardError; end
|
|
18
20
|
|
|
@@ -39,6 +41,8 @@ module Aidp
|
|
|
39
41
|
display_message("\nš¤ Welcome to AIDP Guided Workflow!", type: :highlight)
|
|
40
42
|
display_message("I'll help you plan and execute your project.\n", type: :info)
|
|
41
43
|
|
|
44
|
+
validate_provider_configuration!
|
|
45
|
+
|
|
42
46
|
plan_and_execute_workflow
|
|
43
47
|
rescue => e
|
|
44
48
|
raise ConversationError, "Failed to guide workflow selection: #{e.message}"
|
|
@@ -70,10 +74,18 @@ module Aidp
|
|
|
70
74
|
|
|
71
75
|
@conversation_history << {role: "user", content: goal}
|
|
72
76
|
|
|
77
|
+
iteration = 0
|
|
73
78
|
loop do
|
|
79
|
+
iteration += 1
|
|
74
80
|
# Ask AI for next question based on current plan
|
|
75
81
|
question_response = get_planning_questions(plan)
|
|
76
82
|
|
|
83
|
+
# Debug: show raw provider response and parsed result
|
|
84
|
+
debug_log("Planning iteration #{iteration} provider response", level: :debug, data: {
|
|
85
|
+
raw_response: question_response[:raw_response]&.inspect,
|
|
86
|
+
parsed: question_response.inspect
|
|
87
|
+
})
|
|
88
|
+
|
|
77
89
|
# If AI says plan is complete, confirm with user
|
|
78
90
|
if question_response[:complete]
|
|
79
91
|
display_message("\nā
Plan Summary", type: :highlight)
|
|
@@ -96,6 +108,12 @@ module Aidp
|
|
|
96
108
|
# Update plan with answer
|
|
97
109
|
update_plan_from_answer(plan, question, answer)
|
|
98
110
|
end
|
|
111
|
+
|
|
112
|
+
# Guard: break loop after 10 iterations to avoid infinite loop
|
|
113
|
+
if iteration >= 10
|
|
114
|
+
display_message("[ERROR] Planning loop exceeded 10 iterations. Provider may be returning generic responses.", type: :error)
|
|
115
|
+
break
|
|
116
|
+
end
|
|
99
117
|
end
|
|
100
118
|
|
|
101
119
|
plan
|
|
@@ -105,8 +123,18 @@ module Aidp
|
|
|
105
123
|
system_prompt = build_planning_system_prompt
|
|
106
124
|
user_prompt = build_planning_prompt(plan)
|
|
107
125
|
|
|
126
|
+
# If requirements are already detailed, ask provider to check for completion
|
|
127
|
+
requirements = plan[:requirements]
|
|
128
|
+
requirements_detailed = requirements.is_a?(Hash) && requirements.values.flatten.any? { |r| r.length > 50 }
|
|
129
|
+
if requirements_detailed
|
|
130
|
+
user_prompt += "\n\nNOTE: Requirements have been provided in detail above. If you have enough information, set 'complete' to true. Do not repeat the same requirements question."
|
|
131
|
+
end
|
|
132
|
+
|
|
108
133
|
response = call_provider_for_analysis(system_prompt, user_prompt)
|
|
109
|
-
parse_planning_response(response)
|
|
134
|
+
parsed = parse_planning_response(response)
|
|
135
|
+
# Attach raw response for debug
|
|
136
|
+
parsed[:raw_response] = response
|
|
137
|
+
parsed
|
|
110
138
|
end
|
|
111
139
|
|
|
112
140
|
def identify_steps_from_plan(plan)
|
|
@@ -135,6 +163,16 @@ module Aidp
|
|
|
135
163
|
end
|
|
136
164
|
|
|
137
165
|
def build_workflow_from_plan(plan, needed_steps)
|
|
166
|
+
# Filter out any unknown steps to avoid nil dereference if SPEC changed or an AI hallucinated a step key
|
|
167
|
+
execute_spec = Aidp::Execute::Steps::SPEC
|
|
168
|
+
unknown_steps = needed_steps.reject { |s| execute_spec.key?(s) }
|
|
169
|
+
if unknown_steps.any?
|
|
170
|
+
display_message("ā ļø Ignoring unknown execute steps: #{unknown_steps.join(", ")}", type: :warning)
|
|
171
|
+
needed_steps -= unknown_steps
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
details = needed_steps.map { |step| execute_spec[step]["description"] }
|
|
175
|
+
|
|
138
176
|
{
|
|
139
177
|
mode: :execute,
|
|
140
178
|
workflow_key: :plan_and_execute,
|
|
@@ -144,7 +182,7 @@ module Aidp
|
|
|
144
182
|
workflow: {
|
|
145
183
|
name: "Plan & Execute",
|
|
146
184
|
description: "Custom workflow from iterative planning",
|
|
147
|
-
details:
|
|
185
|
+
details: details
|
|
148
186
|
},
|
|
149
187
|
completion_criteria: plan[:completion_criteria]
|
|
150
188
|
}
|
|
@@ -233,33 +271,52 @@ module Aidp
|
|
|
233
271
|
end
|
|
234
272
|
|
|
235
273
|
def call_provider_for_analysis(system_prompt, user_prompt)
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
attempts = 0
|
|
275
|
+
max_attempts = (@provider_manager.respond_to?(:configured_providers) ? @provider_manager.configured_providers.size : 2)
|
|
276
|
+
max_attempts = 2 if max_attempts < 2 # at least one retry if a fallback exists
|
|
238
277
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
end
|
|
278
|
+
begin
|
|
279
|
+
attempts += 1
|
|
242
280
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
281
|
+
provider_name = @provider_manager.current_provider
|
|
282
|
+
unless provider_name
|
|
283
|
+
raise ConversationError, "No provider configured for guided workflow"
|
|
284
|
+
end
|
|
246
285
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
286
|
+
# Create provider instance using ProviderFactory
|
|
287
|
+
provider_factory = Aidp::Harness::ProviderFactory.new(@config_manager)
|
|
288
|
+
provider = provider_factory.create_provider(provider_name, prompt: @prompt)
|
|
250
289
|
|
|
251
|
-
|
|
252
|
-
|
|
290
|
+
unless provider
|
|
291
|
+
raise ConversationError, "Failed to create provider instance for #{provider_name}"
|
|
292
|
+
end
|
|
253
293
|
|
|
254
|
-
|
|
294
|
+
combined_prompt = "#{system_prompt}\n\n#{user_prompt}"
|
|
295
|
+
result = provider.send(prompt: combined_prompt)
|
|
255
296
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
297
|
+
if result.nil? || result.empty?
|
|
298
|
+
raise ConversationError, "Provider request failed: empty response"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
result
|
|
302
|
+
rescue => e
|
|
303
|
+
message = e.message.to_s
|
|
304
|
+
classified = if message =~ /resource[_ ]exhausted/i || message =~ /\[resource_exhausted\]/i
|
|
305
|
+
"resource_exhausted"
|
|
306
|
+
elsif message =~ /quota[_ ]exceeded/i || message =~ /\[quota_exceeded\]/i
|
|
307
|
+
"quota_exceeded"
|
|
308
|
+
end
|
|
261
309
|
|
|
262
|
-
|
|
310
|
+
if classified && attempts < max_attempts
|
|
311
|
+
display_message("ā ļø Provider '#{provider_name}' #{classified.tr("_", " ")} ā attempting fallback...", type: :warning)
|
|
312
|
+
switched = @provider_manager.switch_provider_for_error(classified, stderr: message) if @provider_manager.respond_to?(:switch_provider_for_error)
|
|
313
|
+
if switched && switched != provider_name
|
|
314
|
+
display_message("ā©ļø Switched to provider '#{switched}'", type: :info)
|
|
315
|
+
retry
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
raise
|
|
319
|
+
end
|
|
263
320
|
end
|
|
264
321
|
|
|
265
322
|
def parse_recommendation(response_text)
|
|
@@ -276,6 +333,22 @@ module Aidp
|
|
|
276
333
|
raise ConversationError, "Invalid JSON in recommendation: #{e.message}"
|
|
277
334
|
end
|
|
278
335
|
|
|
336
|
+
def validate_provider_configuration!
|
|
337
|
+
configured = @provider_manager.configured_providers
|
|
338
|
+
if configured.nil? || configured.empty?
|
|
339
|
+
raise ConversationError, <<~MSG.strip
|
|
340
|
+
No providers are configured. Create an aidp.yml with at least one provider, for example:
|
|
341
|
+
|
|
342
|
+
harness:\n enabled: true\n default_provider: claude\nproviders:\n claude:\n type: api\n api_key: "${AIDP_CLAUDE_API_KEY}"\n models:\n - claude-3-5-sonnet-20241022
|
|
343
|
+
MSG
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
default = @provider_manager.current_provider
|
|
347
|
+
unless default && configured.include?(default)
|
|
348
|
+
raise ConversationError, "Default provider '#{default || "(nil)"}' not found in configured providers: #{configured.join(", ")}"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
279
352
|
def present_recommendation(recommendation)
|
|
280
353
|
display_message("\n⨠Recommendation", type: :highlight)
|
|
281
354
|
display_message("ā" * 60, type: :muted)
|
|
@@ -12,6 +12,9 @@ You are executing a simple, focused task within the AIDP work loop.
|
|
|
12
12
|
2. **Execute the task exactly as described**
|
|
13
13
|
3. **Verify your work** by running any validation commands specified
|
|
14
14
|
4. **Edit this PROMPT.md** to track progress and mark complete when done
|
|
15
|
+
5. **Request follow-up units** by adding `NEXT_UNIT: <unit_name>` to your
|
|
16
|
+
response when deterministic work (tests, linting, wait states) should run
|
|
17
|
+
next
|
|
15
18
|
|
|
16
19
|
## Completion Criteria
|
|
17
20
|
|
|
@@ -34,3 +37,5 @@ STATUS: COMPLETE
|
|
|
34
37
|
- Keep your changes minimal and focused on the task
|
|
35
38
|
- If the task involves running commands, show the command output
|
|
36
39
|
- If the task involves fixing issues, list what was fixed
|
|
40
|
+
- When you need automation to continue after this task, emit `NEXT_UNIT: agentic`
|
|
41
|
+
or a deterministic unit such as `run_full_tests` or `wait_for_github`
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -259,11 +259,12 @@ files:
|
|
|
259
259
|
- lib/aidp/core_ext/class_attribute.rb
|
|
260
260
|
- lib/aidp/daemon/process_manager.rb
|
|
261
261
|
- lib/aidp/daemon/runner.rb
|
|
262
|
-
- lib/aidp/debug_logger.rb
|
|
263
262
|
- lib/aidp/debug_mixin.rb
|
|
263
|
+
- lib/aidp/execute/agent_signal_parser.rb
|
|
264
264
|
- lib/aidp/execute/async_work_loop_runner.rb
|
|
265
265
|
- lib/aidp/execute/checkpoint.rb
|
|
266
266
|
- lib/aidp/execute/checkpoint_display.rb
|
|
267
|
+
- lib/aidp/execute/deterministic_unit.rb
|
|
267
268
|
- lib/aidp/execute/future_work_backlog.rb
|
|
268
269
|
- lib/aidp/execute/guard_policy.rb
|
|
269
270
|
- lib/aidp/execute/instruction_queue.rb
|
|
@@ -275,6 +276,7 @@ files:
|
|
|
275
276
|
- lib/aidp/execute/steps.rb
|
|
276
277
|
- lib/aidp/execute/work_loop_runner.rb
|
|
277
278
|
- lib/aidp/execute/work_loop_state.rb
|
|
279
|
+
- lib/aidp/execute/work_loop_unit_scheduler.rb
|
|
278
280
|
- lib/aidp/execute/workflow_selector.rb
|
|
279
281
|
- lib/aidp/harness/completion_checker.rb
|
|
280
282
|
- lib/aidp/harness/condition_detector.rb
|
|
@@ -337,6 +339,7 @@ files:
|
|
|
337
339
|
- lib/aidp/providers/github_copilot.rb
|
|
338
340
|
- lib/aidp/providers/macos_ui.rb
|
|
339
341
|
- lib/aidp/providers/opencode.rb
|
|
342
|
+
- lib/aidp/rescue_logging.rb
|
|
340
343
|
- lib/aidp/setup/wizard.rb
|
|
341
344
|
- lib/aidp/storage/csv_storage.rb
|
|
342
345
|
- lib/aidp/storage/file_manager.rb
|
data/lib/aidp/debug_logger.rb
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "json"
|
|
5
|
-
require "pastel"
|
|
6
|
-
|
|
7
|
-
module Aidp
|
|
8
|
-
# Debug logger that outputs to both console and a single log file
|
|
9
|
-
class DebugLogger
|
|
10
|
-
LOG_LEVELS = {
|
|
11
|
-
debug: 0,
|
|
12
|
-
info: 1,
|
|
13
|
-
warn: 2,
|
|
14
|
-
error: 3
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
def initialize(log_dir: nil)
|
|
18
|
-
@log_dir = log_dir || default_log_dir
|
|
19
|
-
@log_file = nil
|
|
20
|
-
@run_started = false
|
|
21
|
-
ensure_log_directory
|
|
22
|
-
log_run_banner
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def log(message, level: :info, data: nil)
|
|
26
|
-
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
27
|
-
level_str = level.to_s.upcase.ljust(5)
|
|
28
|
-
|
|
29
|
-
# Format message with timestamp and level
|
|
30
|
-
formatted_message = "[#{timestamp}] #{level_str} #{message}"
|
|
31
|
-
|
|
32
|
-
# Add data if present
|
|
33
|
-
if data && !data.empty?
|
|
34
|
-
data_str = format_data(data)
|
|
35
|
-
formatted_message += "\n#{data_str}" if data_str
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Output to console
|
|
39
|
-
output_to_console(formatted_message, level)
|
|
40
|
-
|
|
41
|
-
# Output to log file
|
|
42
|
-
output_to_file(formatted_message, level, data)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def close
|
|
46
|
-
@log_file&.close
|
|
47
|
-
@log_file = nil
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def default_log_dir
|
|
53
|
-
File.join(Dir.pwd, ".aidp", "debug_logs")
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def ensure_log_directory
|
|
57
|
-
FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def log_file_path
|
|
61
|
-
@log_file_path ||= File.join(@log_dir, "aidp_debug.log")
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def get_log_file
|
|
65
|
-
return @log_file if @log_file && !@log_file.closed?
|
|
66
|
-
|
|
67
|
-
@log_file = File.open(log_file_path, "a")
|
|
68
|
-
@log_file
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def log_run_banner
|
|
72
|
-
return if @run_started
|
|
73
|
-
@run_started = true
|
|
74
|
-
|
|
75
|
-
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
|
76
|
-
command_line = build_command_line
|
|
77
|
-
|
|
78
|
-
banner = <<~BANNER
|
|
79
|
-
|
|
80
|
-
================================================================================
|
|
81
|
-
AIDP DEBUG SESSION STARTED
|
|
82
|
-
================================================================================
|
|
83
|
-
Timestamp: #{timestamp}
|
|
84
|
-
Command: #{command_line}
|
|
85
|
-
Working Directory: #{Dir.pwd}
|
|
86
|
-
Debug Level: #{ENV["DEBUG"] || "0"}
|
|
87
|
-
================================================================================
|
|
88
|
-
BANNER
|
|
89
|
-
|
|
90
|
-
# Write banner to log file
|
|
91
|
-
file = get_log_file
|
|
92
|
-
file.puts banner
|
|
93
|
-
file.flush
|
|
94
|
-
|
|
95
|
-
# Also output to console if debug is enabled
|
|
96
|
-
if ENV["DEBUG"] && ENV["DEBUG"].to_i > 0
|
|
97
|
-
puts "\e[36m#{banner}\e[0m" # Cyan color
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def build_command_line
|
|
102
|
-
# Get the command line arguments
|
|
103
|
-
cmd_parts = []
|
|
104
|
-
|
|
105
|
-
# Add the main command (aidp)
|
|
106
|
-
cmd_parts << "aidp"
|
|
107
|
-
|
|
108
|
-
# Add any arguments from ARGV
|
|
109
|
-
if defined?(ARGV) && !ARGV.empty?
|
|
110
|
-
cmd_parts.concat(ARGV)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Add environment variables that affect behavior
|
|
114
|
-
env_vars = []
|
|
115
|
-
env_vars << "DEBUG=#{ENV["DEBUG"]}" if ENV["DEBUG"]
|
|
116
|
-
env_vars << "AIDP_CONFIG=#{ENV["AIDP_CONFIG"]}" if ENV["AIDP_CONFIG"]
|
|
117
|
-
|
|
118
|
-
if env_vars.any?
|
|
119
|
-
cmd_parts << "(" + env_vars.join(" ") + ")"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
cmd_parts.join(" ")
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def output_to_console(message, level)
|
|
126
|
-
case level
|
|
127
|
-
when :error
|
|
128
|
-
warn message
|
|
129
|
-
when :warn
|
|
130
|
-
puts "\e[33m#{message}\e[0m" # Yellow
|
|
131
|
-
when :info
|
|
132
|
-
puts "\e[36m#{message}\e[0m" # Cyan
|
|
133
|
-
when :debug
|
|
134
|
-
puts "\e[90m#{message}\e[0m" # Gray
|
|
135
|
-
else
|
|
136
|
-
puts message
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def output_to_file(message, level, data)
|
|
141
|
-
file = get_log_file
|
|
142
|
-
file.puts message
|
|
143
|
-
|
|
144
|
-
# Add structured data if present
|
|
145
|
-
if data && !data.empty?
|
|
146
|
-
structured_data = {
|
|
147
|
-
timestamp: Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N%z"),
|
|
148
|
-
level: level.to_s,
|
|
149
|
-
message: message,
|
|
150
|
-
data: data
|
|
151
|
-
}
|
|
152
|
-
file.puts "DATA: #{JSON.generate(structured_data)}"
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
file.flush
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def format_data(data)
|
|
159
|
-
return nil if data.nil? || data.empty?
|
|
160
|
-
|
|
161
|
-
case data
|
|
162
|
-
when Hash
|
|
163
|
-
format_hash_data(data)
|
|
164
|
-
when Array
|
|
165
|
-
format_array_data(data)
|
|
166
|
-
when String
|
|
167
|
-
(data.length > 200) ? "#{data[0..200]}..." : data
|
|
168
|
-
else
|
|
169
|
-
data.to_s
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def format_hash_data(hash)
|
|
174
|
-
lines = []
|
|
175
|
-
hash.each do |key, value|
|
|
176
|
-
lines << if value.is_a?(String) && value.length > 100
|
|
177
|
-
" #{key}: #{value[0..100]}..."
|
|
178
|
-
elsif value.is_a?(Hash) || value.is_a?(Array)
|
|
179
|
-
" #{key}: #{JSON.pretty_generate(value).gsub("\n", "\n ")}"
|
|
180
|
-
else
|
|
181
|
-
" #{key}: #{value}"
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
lines.join("\n")
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def format_array_data(array)
|
|
188
|
-
if array.length > 10
|
|
189
|
-
"#{array.first(5).join(", ")}... (#{array.length} total items)"
|
|
190
|
-
else
|
|
191
|
-
array.join(", ")
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|