aidp 0.17.1 → 0.19.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -0
  3. data/lib/aidp/cli/terminal_io.rb +5 -2
  4. data/lib/aidp/cli.rb +43 -2
  5. data/lib/aidp/config.rb +9 -14
  6. data/lib/aidp/execute/agent_signal_parser.rb +20 -0
  7. data/lib/aidp/execute/persistent_tasklist.rb +220 -0
  8. data/lib/aidp/execute/prompt_manager.rb +128 -1
  9. data/lib/aidp/execute/repl_macros.rb +719 -0
  10. data/lib/aidp/execute/work_loop_runner.rb +162 -1
  11. data/lib/aidp/harness/ai_decision_engine.rb +376 -0
  12. data/lib/aidp/harness/capability_registry.rb +273 -0
  13. data/lib/aidp/harness/config_schema.rb +305 -1
  14. data/lib/aidp/harness/configuration.rb +452 -0
  15. data/lib/aidp/harness/enhanced_runner.rb +7 -1
  16. data/lib/aidp/harness/provider_factory.rb +0 -2
  17. data/lib/aidp/harness/runner.rb +7 -1
  18. data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
  19. data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
  20. data/lib/aidp/init/devcontainer_generator.rb +274 -0
  21. data/lib/aidp/init/runner.rb +37 -10
  22. data/lib/aidp/init.rb +1 -0
  23. data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
  24. data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
  25. data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
  26. data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
  27. data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
  28. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
  29. data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
  30. data/lib/aidp/provider_manager.rb +0 -2
  31. data/lib/aidp/providers/anthropic.rb +19 -0
  32. data/lib/aidp/setup/wizard.rb +299 -4
  33. data/lib/aidp/utils/devcontainer_detector.rb +166 -0
  34. data/lib/aidp/version.rb +1 -1
  35. data/lib/aidp/watch/build_processor.rb +72 -6
  36. data/lib/aidp/watch/repository_client.rb +2 -1
  37. data/lib/aidp.rb +1 -1
  38. data/templates/aidp.yml.example +128 -0
  39. metadata +15 -2
  40. data/lib/aidp/providers/macos_ui.rb +0 -102
@@ -46,11 +46,12 @@ module Aidp
46
46
  @project_dir = project_dir
47
47
  @provider_manager = provider_manager
48
48
  @config = config
49
- @prompt_manager = PromptManager.new(project_dir)
49
+ @prompt_manager = PromptManager.new(project_dir, config: config)
50
50
  @test_runner = Aidp::Harness::TestRunner.new(project_dir, config)
51
51
  @checkpoint = Checkpoint.new(project_dir)
52
52
  @checkpoint_display = CheckpointDisplay.new
53
53
  @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
54
+ @persistent_tasklist = PersistentTasklist.new(project_dir)
54
55
  @iteration_count = 0
55
56
  @step_name = nil
56
57
  @options = options
@@ -74,6 +75,7 @@ module Aidp
74
75
  display_message(" Flow: Deterministic ↔ Agentic with fix-forward core", type: :info)
75
76
 
76
77
  display_guard_policy_status
78
+ display_pending_tasks
77
79
 
78
80
  @unit_scheduler = WorkLoopUnitScheduler.new(units_config)
79
81
  base_context = context.dup
@@ -148,6 +150,9 @@ module Aidp
148
150
  transition_to(:apply_patch)
149
151
  agent_result = apply_patch
150
152
 
153
+ # Process agent output for task filing signals
154
+ process_task_filing(agent_result)
155
+
151
156
  transition_to(:test)
152
157
  test_results = @test_runner.run_tests
153
158
  lint_results = @test_runner.run_linters
@@ -343,6 +348,16 @@ module Aidp
343
348
 
344
349
  # Create initial PROMPT.md with all context
345
350
  def create_initial_prompt(step_spec, context)
351
+ # Try intelligent prompt optimization first (ZFC-powered)
352
+ if @prompt_manager.optimization_enabled?
353
+ if create_optimized_prompt(step_spec, context)
354
+ return
355
+ end
356
+ # Fallback to traditional prompt on optimization failure
357
+ display_message(" ⚠️ Prompt optimization failed, using traditional approach", type: :warning)
358
+ end
359
+
360
+ # Traditional prompt building (fallback or when optimization disabled)
346
361
  template_content = load_template(step_spec["templates"]&.first)
347
362
  prd_content = load_prd
348
363
  style_guide = load_style_guide
@@ -364,6 +379,103 @@ module Aidp
364
379
  display_message(" Created PROMPT.md (#{initial_prompt.length} chars)", type: :info)
365
380
  end
366
381
 
382
+ # Create prompt using intelligent optimization (Zero Framework Cognition)
383
+ # Selects only the most relevant fragments from style guide, templates, and code
384
+ def create_optimized_prompt(step_spec, context)
385
+ user_input = format_user_input(context[:user_input])
386
+
387
+ # Infer task type from step name
388
+ task_type = infer_task_type(step_spec, user_input)
389
+
390
+ # Extract affected files from context or PRD
391
+ affected_files = extract_affected_files(context, user_input)
392
+
393
+ # Build task context for optimizer
394
+ task_context = {
395
+ task_type: task_type,
396
+ description: build_task_description(user_input, context),
397
+ affected_files: affected_files,
398
+ step_name: @step_name,
399
+ tags: extract_tags(user_input, step_spec)
400
+ }
401
+
402
+ # Use optimizer to create prompt
403
+ success = @prompt_manager.write_optimized(
404
+ task_context,
405
+ include_metadata: @config.prompt_log_fragments?
406
+ )
407
+
408
+ if success
409
+ stats = @prompt_manager.last_optimization_stats
410
+ display_message(" ✨ Created optimized PROMPT.md", type: :success)
411
+ display_message(" Selected: #{stats.selected_count} fragments, Excluded: #{stats.excluded_count}", type: :info)
412
+ display_message(" Tokens: #{stats.total_tokens} (#{stats.budget_utilization.round(1)}% of budget)", type: :info)
413
+ display_message(" Avg relevance: #{(stats.average_score * 100).round(1)}%", type: :info)
414
+ end
415
+
416
+ success
417
+ end
418
+
419
+ # Infer task type from step name and context
420
+ def infer_task_type(step_spec, user_input)
421
+ step_name = @step_name.to_s.downcase
422
+ input_lower = user_input.to_s.downcase
423
+
424
+ return :test if step_name.include?("test") || input_lower.include?("test")
425
+ return :bugfix if step_name.include?("fix") || input_lower.include?("fix") || input_lower.include?("bug")
426
+ return :refactor if step_name.include?("refactor") || input_lower.include?("refactor")
427
+ return :analysis if step_name.include?("analyz") || step_name.include?("review")
428
+
429
+ :feature # Default to feature
430
+ end
431
+
432
+ # Extract files that will be affected by this work
433
+ def extract_affected_files(context, user_input)
434
+ files = []
435
+
436
+ # From user input (e.g., "update lib/user.rb")
437
+ user_input&.scan(/[\w\/]+\.rb/)&.each do |file|
438
+ files << file
439
+ end
440
+
441
+ # From deterministic outputs
442
+ context[:deterministic_outputs]&.each do |output|
443
+ if output[:output_path]&.end_with?(".rb")
444
+ files << output[:output_path]
445
+ end
446
+ end
447
+
448
+ files.uniq
449
+ end
450
+
451
+ # Build task description from context
452
+ def build_task_description(user_input, context)
453
+ parts = []
454
+ parts << user_input if user_input && !user_input.empty?
455
+ parts << context[:previous_agent_summary] if context[:previous_agent_summary]
456
+ parts.join("\n\n")
457
+ end
458
+
459
+ # Extract relevant tags from input and spec
460
+ def extract_tags(user_input, step_spec)
461
+ tags = []
462
+ input_lower = user_input.to_s.downcase
463
+
464
+ # Common tags from content
465
+ tags << "testing" if input_lower.include?("test")
466
+ tags << "security" if input_lower.include?("security") || input_lower.include?("auth")
467
+ tags << "api" if input_lower.include?("api") || input_lower.include?("endpoint")
468
+ tags << "database" if input_lower.include?("database") || input_lower.include?("migration")
469
+ tags << "performance" if input_lower.include?("performance") || input_lower.include?("optim")
470
+
471
+ # Tags from step spec
472
+ if step_spec["tags"]
473
+ tags.concat(Array(step_spec["tags"]))
474
+ end
475
+
476
+ tags.uniq
477
+ end
478
+
367
479
  def build_initial_prompt_content(template:, prd:, style_guide:, user_input:, step_name:, deterministic_outputs:, previous_agent_summary:)
368
480
  parts = []
369
481
 
@@ -669,6 +781,55 @@ module Aidp
669
781
  display_message("")
670
782
  end
671
783
 
784
+ # Display pending tasks from persistent tasklist
785
+ def display_pending_tasks
786
+ pending_tasks = @persistent_tasklist.pending
787
+ return if pending_tasks.empty?
788
+
789
+ display_message("\n📋 Pending Tasks from Previous Sessions:", type: :info)
790
+
791
+ # Show up to 5 most recent pending tasks
792
+ pending_tasks.take(5).each do |task|
793
+ priority_icon = case task.priority
794
+ when :high then "⚠️ "
795
+ when :medium then "○ "
796
+ when :low then "· "
797
+ end
798
+
799
+ age = ((Time.now - task.created_at) / 86400).to_i
800
+ age_str = (age > 0) ? " (#{age}d ago)" : " (today)"
801
+
802
+ display_message(" #{priority_icon}#{task.description}#{age_str}", type: :info)
803
+ end
804
+
805
+ if pending_tasks.size > 5
806
+ display_message(" ... and #{pending_tasks.size - 5} more. Use /tasks list to see all", type: :info)
807
+ end
808
+
809
+ display_message("")
810
+ end
811
+
812
+ # Process agent output for task filing signals
813
+ def process_task_filing(agent_result)
814
+ return unless agent_result && agent_result[:output]
815
+
816
+ filed_tasks = AgentSignalParser.parse_task_filing(agent_result[:output])
817
+ return if filed_tasks.empty?
818
+
819
+ filed_tasks.each do |task_data|
820
+ task = @persistent_tasklist.create(
821
+ task_data[:description],
822
+ priority: task_data[:priority],
823
+ session: @step_name,
824
+ discovered_during: "#{@step_name} iteration #{@iteration_count}",
825
+ tags: task_data[:tags]
826
+ )
827
+
828
+ Aidp.log_info("tasklist", "Filed new task from agent", task_id: task.id, description: task.description)
829
+ display_message("📋 Filed task: #{task.description} (#{task.id})", type: :info)
830
+ end
831
+ end
832
+
672
833
  # Validate changes against guard policy
673
834
  # Returns validation result with errors if any
674
835
  def validate_guard_policy(changed_files = [])
@@ -0,0 +1,376 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "provider_factory"
5
+ require_relative "thinking_depth_manager"
6
+
7
+ module Aidp
8
+ module Harness
9
+ # Zero Framework Cognition (ZFC) Decision Engine
10
+ #
11
+ # Delegates semantic analysis and decision-making to AI models instead of
12
+ # using brittle pattern matching, scoring formulas, or heuristic thresholds.
13
+ #
14
+ # @example Basic usage
15
+ # engine = AIDecisionEngine.new(config, provider_manager)
16
+ # result = engine.decide(:condition_detection,
17
+ # context: { error: "Rate limit exceeded" },
18
+ # schema: ConditionSchema,
19
+ # tier: "mini"
20
+ # )
21
+ # # => { condition: "rate_limit", confidence: 0.95, reasoning: "..." }
22
+ #
23
+ # @see docs/ZFC_COMPLIANCE_ASSESSMENT.md
24
+ # @see docs/ZFC_IMPLEMENTATION_PLAN.md
25
+ class AIDecisionEngine
26
+ # Decision templates define prompts, schemas, and defaults for each decision type
27
+ DECISION_TEMPLATES = {
28
+ condition_detection: {
29
+ prompt_template: <<~PROMPT,
30
+ Analyze the following API response or error message and classify the condition.
31
+
32
+ Response/Error:
33
+ {{response}}
34
+
35
+ Classify this into one of the following conditions:
36
+ - rate_limit: API rate limiting or quota exceeded
37
+ - auth_error: Authentication or authorization failure
38
+ - timeout: Request timeout or network timeout
39
+ - completion_marker: Work is complete or done
40
+ - user_feedback_needed: AI is asking for user input/clarification
41
+ - api_error: General API error (not rate limit/auth)
42
+ - success: Successful response
43
+ - other: None of the above
44
+
45
+ Provide your classification with a confidence score (0.0 to 1.0) and brief reasoning.
46
+ PROMPT
47
+ schema: {
48
+ type: "object",
49
+ properties: {
50
+ condition: {
51
+ type: "string",
52
+ enum: [
53
+ "rate_limit",
54
+ "auth_error",
55
+ "timeout",
56
+ "completion_marker",
57
+ "user_feedback_needed",
58
+ "api_error",
59
+ "success",
60
+ "other"
61
+ ]
62
+ },
63
+ confidence: {
64
+ type: "number",
65
+ minimum: 0.0,
66
+ maximum: 1.0
67
+ },
68
+ reasoning: {
69
+ type: "string"
70
+ }
71
+ },
72
+ required: ["condition", "confidence"]
73
+ },
74
+ default_tier: "mini",
75
+ cache_ttl: nil # Each response is unique
76
+ },
77
+
78
+ error_classification: {
79
+ prompt_template: <<~PROMPT,
80
+ Classify the following error and determine if it's retryable.
81
+
82
+ Error:
83
+ {{error_message}}
84
+
85
+ Context:
86
+ {{context}}
87
+
88
+ Determine:
89
+ 1. Error type (rate_limit, auth, timeout, network, api_bug, other)
90
+ 2. Whether it's retryable (transient vs permanent)
91
+ 3. Recommended action (retry, switch_provider, escalate, fail)
92
+
93
+ Provide classification with confidence and reasoning.
94
+ PROMPT
95
+ schema: {
96
+ type: "object",
97
+ properties: {
98
+ error_type: {
99
+ type: "string",
100
+ enum: ["rate_limit", "auth", "timeout", "network", "api_bug", "other"]
101
+ },
102
+ retryable: {
103
+ type: "boolean"
104
+ },
105
+ recommended_action: {
106
+ type: "string",
107
+ enum: ["retry", "switch_provider", "escalate", "fail"]
108
+ },
109
+ confidence: {
110
+ type: "number",
111
+ minimum: 0.0,
112
+ maximum: 1.0
113
+ },
114
+ reasoning: {
115
+ type: "string"
116
+ }
117
+ },
118
+ required: ["error_type", "retryable", "recommended_action", "confidence"]
119
+ },
120
+ default_tier: "mini",
121
+ cache_ttl: nil
122
+ },
123
+
124
+ completion_detection: {
125
+ prompt_template: <<~PROMPT,
126
+ Determine if the work described is complete based on the AI response.
127
+
128
+ Task:
129
+ {{task_description}}
130
+
131
+ AI Response:
132
+ {{response}}
133
+
134
+ Is the work complete? Consider:
135
+ - Explicit completion markers ("done", "finished", etc.)
136
+ - Implicit indicators (results provided, no follow-up questions)
137
+ - Requests for more information (incomplete)
138
+
139
+ Provide boolean completion status with confidence and reasoning.
140
+ PROMPT
141
+ schema: {
142
+ type: "object",
143
+ properties: {
144
+ complete: {
145
+ type: "boolean"
146
+ },
147
+ confidence: {
148
+ type: "number",
149
+ minimum: 0.0,
150
+ maximum: 1.0
151
+ },
152
+ reasoning: {
153
+ type: "string"
154
+ }
155
+ },
156
+ required: ["complete", "confidence"]
157
+ },
158
+ default_tier: "mini",
159
+ cache_ttl: nil
160
+ }
161
+ }.freeze
162
+
163
+ attr_reader :config, :provider_factory, :cache
164
+
165
+ # Initialize the AI Decision Engine
166
+ #
167
+ # @param config [Configuration] AIDP configuration object
168
+ # @param provider_factory [ProviderFactory] Factory for creating provider instances
169
+ def initialize(config, provider_factory: nil)
170
+ @config = config
171
+ @provider_factory = provider_factory || ProviderFactory.new(config)
172
+ @cache = {}
173
+ @cache_timestamps = {}
174
+ end
175
+
176
+ # Make an AI-powered decision
177
+ #
178
+ # @param decision_type [Symbol] Type of decision (:condition_detection, :error_classification, etc.)
179
+ # @param context [Hash] Context data for the decision
180
+ # @param schema [Hash, nil] JSON schema for response validation (overrides default)
181
+ # @param tier [String, nil] Thinking depth tier (overrides default)
182
+ # @param cache_ttl [Integer, nil] Cache TTL in seconds (overrides default)
183
+ # @return [Hash] Validated decision result
184
+ # @raise [ArgumentError] If decision_type is unknown
185
+ # @raise [ValidationError] If response doesn't match schema
186
+ def decide(decision_type, context:, schema: nil, tier: nil, cache_ttl: nil)
187
+ template = DECISION_TEMPLATES[decision_type]
188
+ raise ArgumentError, "Unknown decision type: #{decision_type}" unless template
189
+
190
+ # Check cache if TTL specified
191
+ cache_key = build_cache_key(decision_type, context)
192
+ ttl = cache_ttl || template[:cache_ttl]
193
+ if ttl && (cached_result = get_cached(cache_key, ttl))
194
+ Aidp.log_debug("ai_decision_engine", "Cache hit for #{decision_type}", {
195
+ cache_key: cache_key,
196
+ ttl: ttl
197
+ })
198
+ return cached_result
199
+ end
200
+
201
+ # Build prompt from template
202
+ prompt = build_prompt(template[:prompt_template], context)
203
+
204
+ # Select tier
205
+ selected_tier = tier || template[:default_tier]
206
+
207
+ # Get model for tier
208
+ thinking_manager = ThinkingDepthManager.new(config)
209
+ provider_name, model_name, _model_data = thinking_manager.select_model_for_tier(selected_tier)
210
+
211
+ Aidp.log_debug("ai_decision_engine", "Making AI decision", {
212
+ decision_type: decision_type,
213
+ tier: selected_tier,
214
+ provider: provider_name,
215
+ model: model_name,
216
+ cache_ttl: ttl
217
+ })
218
+
219
+ # Call AI with schema validation
220
+ response_schema = schema || template[:schema]
221
+ result = call_ai_with_schema(provider_name, model_name, prompt, response_schema)
222
+
223
+ # Validate result
224
+ validate_schema(result, response_schema)
225
+
226
+ # Cache if TTL specified
227
+ if ttl
228
+ set_cached(cache_key, result)
229
+ end
230
+
231
+ result
232
+ end
233
+
234
+ private
235
+
236
+ # Build cache key from decision type and context
237
+ def build_cache_key(decision_type, context)
238
+ # Simple hash-based key
239
+ "#{decision_type}:#{context.hash}"
240
+ end
241
+
242
+ # Get cached result if still valid
243
+ def get_cached(key, ttl)
244
+ return nil unless @cache.key?(key)
245
+ return nil if Time.now - @cache_timestamps[key] > ttl
246
+ @cache[key]
247
+ end
248
+
249
+ # Store result in cache
250
+ def set_cached(key, value)
251
+ @cache[key] = value
252
+ @cache_timestamps[key] = Time.now
253
+ end
254
+
255
+ # Build prompt from template with context substitution
256
+ def build_prompt(template, context)
257
+ prompt = template.dup
258
+ context.each do |key, value|
259
+ prompt.gsub!("{{#{key}}}", value.to_s)
260
+ end
261
+ prompt
262
+ end
263
+
264
+ # Call AI with schema validation using structured output
265
+ def call_ai_with_schema(provider_name, model_name, prompt, schema)
266
+ # Create provider instance
267
+ provider_options = {
268
+ model: model_name,
269
+ output: nil, # No output for background decisions
270
+ prompt: nil # No TTY prompt needed
271
+ }
272
+
273
+ provider = @provider_factory.create_provider(provider_name, provider_options)
274
+
275
+ # Build enhanced prompt requesting JSON output
276
+ enhanced_prompt = <<~PROMPT
277
+ #{prompt}
278
+
279
+ IMPORTANT: Respond with ONLY valid JSON. No additional text or explanation.
280
+ The JSON must match this structure: #{JSON.generate(schema[:properties].keys)}
281
+ PROMPT
282
+
283
+ # Call provider
284
+ response = provider.send_message(prompt: enhanced_prompt, session: nil)
285
+
286
+ # Parse JSON response
287
+ begin
288
+ # Response might be a string or already structured
289
+ response_text = response.is_a?(String) ? response : response.to_s
290
+
291
+ # Try to extract JSON if there's extra text
292
+ # Use non-greedy match and handle nested braces
293
+ json_match = response_text.match(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/m) || response_text.match(/\{.*\}/m)
294
+ json_text = json_match ? json_match[0] : response_text
295
+
296
+ result = JSON.parse(json_text, symbolize_names: true)
297
+
298
+ Aidp.log_debug("ai_decision_engine", "Parsed JSON successfully", {
299
+ response_length: response_text.length,
300
+ json_length: json_text.length,
301
+ result_keys: result.keys,
302
+ provider: provider_name
303
+ })
304
+
305
+ result
306
+ rescue JSON::ParserError => e
307
+ Aidp.log_error("ai_decision_engine", "Failed to parse AI response as JSON", {
308
+ error: e.message,
309
+ response: response_text&.slice(0, 200),
310
+ provider: provider_name,
311
+ model: model_name
312
+ })
313
+ raise ValidationError, "AI response is not valid JSON: #{e.message}"
314
+ end
315
+ rescue => e
316
+ Aidp.log_error("ai_decision_engine", "Error calling AI provider", {
317
+ error: e.message,
318
+ provider: provider_name,
319
+ model: model_name,
320
+ error_class: e.class.name
321
+ })
322
+ raise
323
+ end
324
+
325
+ # Validate response against JSON schema
326
+ def validate_schema(result, schema)
327
+ # Basic validation of required fields and types
328
+ # Schema uses string keys, but our result uses symbol keys from JSON parsing
329
+ schema[:required]&.each do |field|
330
+ field_sym = field.to_sym
331
+ unless result.key?(field_sym)
332
+ raise ValidationError, "Missing required field: #{field}"
333
+ end
334
+ end
335
+
336
+ schema[:properties]&.each do |field, constraints|
337
+ field_sym = field.to_sym
338
+ next unless result.key?(field_sym)
339
+ value = result[field_sym]
340
+
341
+ # Type validation
342
+ case constraints[:type]
343
+ when "string"
344
+ unless value.is_a?(String)
345
+ raise ValidationError, "Field #{field} must be string, got #{value.class}"
346
+ end
347
+ # Enum validation
348
+ if constraints[:enum] && !constraints[:enum].include?(value)
349
+ raise ValidationError, "Field #{field} must be one of #{constraints[:enum]}, got #{value}"
350
+ end
351
+ when "number"
352
+ unless value.is_a?(Numeric)
353
+ raise ValidationError, "Field #{field} must be number, got #{value.class}"
354
+ end
355
+ # Range validation
356
+ if constraints[:minimum] && value < constraints[:minimum]
357
+ raise ValidationError, "Field #{field} must be >= #{constraints[:minimum]}"
358
+ end
359
+ if constraints[:maximum] && value > constraints[:maximum]
360
+ raise ValidationError, "Field #{field} must be <= #{constraints[:maximum]}"
361
+ end
362
+ when "boolean"
363
+ unless [true, false].include?(value)
364
+ raise ValidationError, "Field #{field} must be boolean, got #{value.class}"
365
+ end
366
+ end
367
+ end
368
+
369
+ true
370
+ end
371
+ end
372
+
373
+ # Validation error for schema violations
374
+ class ValidationError < StandardError; end
375
+ end
376
+ end