aidp 0.13.0 → 0.14.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/lib/aidp/cli/first_run_wizard.rb +28 -303
  4. data/lib/aidp/cli/issue_importer.rb +359 -0
  5. data/lib/aidp/cli.rb +151 -3
  6. data/lib/aidp/daemon/process_manager.rb +146 -0
  7. data/lib/aidp/daemon/runner.rb +232 -0
  8. data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
  9. data/lib/aidp/execute/future_work_backlog.rb +411 -0
  10. data/lib/aidp/execute/guard_policy.rb +246 -0
  11. data/lib/aidp/execute/instruction_queue.rb +131 -0
  12. data/lib/aidp/execute/interactive_repl.rb +335 -0
  13. data/lib/aidp/execute/repl_macros.rb +651 -0
  14. data/lib/aidp/execute/steps.rb +8 -0
  15. data/lib/aidp/execute/work_loop_runner.rb +322 -36
  16. data/lib/aidp/execute/work_loop_state.rb +162 -0
  17. data/lib/aidp/harness/config_schema.rb +88 -0
  18. data/lib/aidp/harness/configuration.rb +48 -1
  19. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +2 -0
  20. data/lib/aidp/init/doc_generator.rb +256 -0
  21. data/lib/aidp/init/project_analyzer.rb +343 -0
  22. data/lib/aidp/init/runner.rb +83 -0
  23. data/lib/aidp/init.rb +5 -0
  24. data/lib/aidp/logger.rb +279 -0
  25. data/lib/aidp/setup/wizard.rb +777 -0
  26. data/lib/aidp/tooling_detector.rb +115 -0
  27. data/lib/aidp/version.rb +1 -1
  28. data/lib/aidp/watch/build_processor.rb +282 -0
  29. data/lib/aidp/watch/plan_generator.rb +166 -0
  30. data/lib/aidp/watch/plan_processor.rb +83 -0
  31. data/lib/aidp/watch/repository_client.rb +243 -0
  32. data/lib/aidp/watch/runner.rb +93 -0
  33. data/lib/aidp/watch/state_store.rb +105 -0
  34. data/lib/aidp/watch.rb +9 -0
  35. data/lib/aidp.rb +14 -0
  36. data/templates/implementation/simple_task.md +36 -0
  37. metadata +26 -1
@@ -0,0 +1,651 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Execute
5
+ # REPL macros for fine-grained human control during work loops
6
+ # Provides commands for:
7
+ # - /pin <file> - Mark files as read-only
8
+ # - /focus <dir|glob> - Restrict work scope
9
+ # - /split - Divide work into smaller contracts
10
+ # - /halt-on <pattern> - Pause on specific test failures
11
+ class ReplMacros
12
+ attr_reader :pinned_files, :focus_patterns, :halt_patterns, :split_mode
13
+
14
+ def initialize
15
+ @pinned_files = Set.new
16
+ @focus_patterns = []
17
+ @halt_patterns = []
18
+ @split_mode = false
19
+ @commands = register_commands
20
+ end
21
+
22
+ # Parse and execute a REPL command
23
+ # Returns { success: boolean, message: string, action: symbol }
24
+ def execute(command_line)
25
+ return {success: false, message: "Empty command", action: :none} if command_line.nil? || command_line.strip.empty?
26
+
27
+ parts = command_line.strip.split(/\s+/)
28
+ command = parts[0]
29
+ args = parts[1..]
30
+
31
+ if command.start_with?("/")
32
+ execute_macro(command, args)
33
+ else
34
+ {success: false, message: "Unknown command: #{command}", action: :none}
35
+ end
36
+ end
37
+
38
+ # Check if a file is pinned (read-only)
39
+ def pinned?(file_path)
40
+ @pinned_files.include?(normalize_path(file_path))
41
+ end
42
+
43
+ # Check if a file matches focus scope
44
+ def in_focus?(file_path)
45
+ return true if @focus_patterns.empty? # No focus = all files in scope
46
+
47
+ normalized = normalize_path(file_path)
48
+ @focus_patterns.any? { |pattern| matches_pattern?(normalized, pattern) }
49
+ end
50
+
51
+ # Check if a test failure should trigger a halt
52
+ def should_halt?(failure_message)
53
+ return false if @halt_patterns.empty?
54
+
55
+ @halt_patterns.any? { |pattern| failure_message.match?(Regexp.new(pattern, Regexp::IGNORECASE)) }
56
+ end
57
+
58
+ # Get summary of current REPL state
59
+ def summary
60
+ {
61
+ pinned_files: @pinned_files.to_a,
62
+ focus_patterns: @focus_patterns,
63
+ halt_patterns: @halt_patterns,
64
+ split_mode: @split_mode,
65
+ active_constraints: active_constraints_count
66
+ }
67
+ end
68
+
69
+ # Clear all macros
70
+ def reset!
71
+ @pinned_files.clear
72
+ @focus_patterns.clear
73
+ @halt_patterns.clear
74
+ @split_mode = false
75
+ end
76
+
77
+ # List all available commands
78
+ def list_commands
79
+ @commands.keys.sort
80
+ end
81
+
82
+ # Get help for a specific command
83
+ def help(command = nil)
84
+ if command.nil?
85
+ @commands.map { |cmd, info| "#{cmd}: #{info[:description]}" }.join("\n")
86
+ elsif @commands.key?(command)
87
+ info = @commands[command]
88
+ "#{command}: #{info[:description]}\nUsage: #{info[:usage]}\nExample: #{info[:example]}"
89
+ else
90
+ "Unknown command: #{command}"
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # Register all available REPL commands
97
+ def register_commands
98
+ {
99
+ "/pin" => {
100
+ description: "Mark file(s) as read-only (do not modify)",
101
+ usage: "/pin <file|glob>",
102
+ example: "/pin config/database.yml",
103
+ handler: method(:cmd_pin)
104
+ },
105
+ "/unpin" => {
106
+ description: "Remove read-only protection from file(s)",
107
+ usage: "/unpin <file|glob>",
108
+ example: "/unpin config/database.yml",
109
+ handler: method(:cmd_unpin)
110
+ },
111
+ "/focus" => {
112
+ description: "Restrict work scope to specific files/directories",
113
+ usage: "/focus <dir|glob>",
114
+ example: "/focus lib/features/auth/**/*",
115
+ handler: method(:cmd_focus)
116
+ },
117
+ "/unfocus" => {
118
+ description: "Remove focus restriction",
119
+ usage: "/unfocus",
120
+ example: "/unfocus",
121
+ handler: method(:cmd_unfocus)
122
+ },
123
+ "/split" => {
124
+ description: "Divide current work into smaller contracts",
125
+ usage: "/split",
126
+ example: "/split",
127
+ handler: method(:cmd_split)
128
+ },
129
+ "/halt-on" => {
130
+ description: "Pause work loop on specific test failure pattern",
131
+ usage: "/halt-on <pattern>",
132
+ example: "/halt-on 'authentication.*failed'",
133
+ handler: method(:cmd_halt_on)
134
+ },
135
+ "/unhalt" => {
136
+ description: "Remove halt-on pattern",
137
+ usage: "/unhalt <pattern>",
138
+ example: "/unhalt 'authentication.*failed'",
139
+ handler: method(:cmd_unhalt)
140
+ },
141
+ "/pause" => {
142
+ description: "Pause the running work loop",
143
+ usage: "/pause",
144
+ example: "/pause",
145
+ handler: method(:cmd_pause)
146
+ },
147
+ "/resume" => {
148
+ description: "Resume a paused work loop",
149
+ usage: "/resume",
150
+ example: "/resume",
151
+ handler: method(:cmd_resume)
152
+ },
153
+ "/cancel" => {
154
+ description: "Cancel the work loop and save checkpoint",
155
+ usage: "/cancel [--no-checkpoint]",
156
+ example: "/cancel",
157
+ handler: method(:cmd_cancel)
158
+ },
159
+ "/inject" => {
160
+ description: "Add instruction to be merged in next iteration",
161
+ usage: "/inject <instruction> [--priority high|normal|low]",
162
+ example: "/inject 'Add error handling for edge case X'",
163
+ handler: method(:cmd_inject)
164
+ },
165
+ "/merge" => {
166
+ description: "Update plan/contract for next iteration",
167
+ usage: "/merge <plan_update>",
168
+ example: "/merge 'Add acceptance criteria: handle timeouts'",
169
+ handler: method(:cmd_merge)
170
+ },
171
+ "/update" => {
172
+ description: "Update guard rail configuration",
173
+ usage: "/update guard <key>=<value>",
174
+ example: "/update guard max_lines=500",
175
+ handler: method(:cmd_update)
176
+ },
177
+ "/reload" => {
178
+ description: "Reload configuration from file",
179
+ usage: "/reload config",
180
+ example: "/reload config",
181
+ handler: method(:cmd_reload)
182
+ },
183
+ "/rollback" => {
184
+ description: "Rollback n commits on current branch",
185
+ usage: "/rollback <n>",
186
+ example: "/rollback 2",
187
+ handler: method(:cmd_rollback)
188
+ },
189
+ "/undo" => {
190
+ description: "Undo last commit",
191
+ usage: "/undo last",
192
+ example: "/undo last",
193
+ handler: method(:cmd_undo)
194
+ },
195
+ "/background" => {
196
+ description: "Detach REPL and enter background daemon mode",
197
+ usage: "/background",
198
+ example: "/background",
199
+ handler: method(:cmd_background)
200
+ },
201
+ "/status" => {
202
+ description: "Show current REPL macro state",
203
+ usage: "/status",
204
+ example: "/status",
205
+ handler: method(:cmd_status)
206
+ },
207
+ "/reset" => {
208
+ description: "Clear all REPL macros",
209
+ usage: "/reset",
210
+ example: "/reset",
211
+ handler: method(:cmd_reset)
212
+ },
213
+ "/help" => {
214
+ description: "Show help for commands",
215
+ usage: "/help [command]",
216
+ example: "/help pin",
217
+ handler: method(:cmd_help)
218
+ }
219
+ }
220
+ end
221
+
222
+ # Execute a macro command
223
+ def execute_macro(command, args)
224
+ command_info = @commands[command]
225
+
226
+ unless command_info
227
+ return {
228
+ success: false,
229
+ message: "Unknown command: #{command}. Type /help for available commands.",
230
+ action: :none
231
+ }
232
+ end
233
+
234
+ command_info[:handler].call(args)
235
+ end
236
+
237
+ # Command: /pin <file|glob>
238
+ def cmd_pin(args)
239
+ return {success: false, message: "Usage: /pin <file|glob>", action: :none} if args.empty?
240
+
241
+ pattern = args.join(" ")
242
+ files = expand_pattern(pattern)
243
+
244
+ if files.empty?
245
+ @pinned_files.add(normalize_path(pattern))
246
+ files_added = [pattern]
247
+ else
248
+ files.each { |f| @pinned_files.add(f) }
249
+ files_added = files
250
+ end
251
+
252
+ {
253
+ success: true,
254
+ message: "Pinned #{files_added.size} file(s): #{files_added.join(", ")}",
255
+ action: :update_constraints,
256
+ data: {pinned: files_added}
257
+ }
258
+ end
259
+
260
+ # Command: /unpin <file|glob>
261
+ def cmd_unpin(args)
262
+ return {success: false, message: "Usage: /unpin <file|glob>", action: :none} if args.empty?
263
+
264
+ pattern = args.join(" ")
265
+ files = expand_pattern(pattern)
266
+
267
+ removed = []
268
+ if files.empty?
269
+ normalized = normalize_path(pattern)
270
+ # Check if file is pinned before attempting to remove
271
+ if @pinned_files.include?(normalized)
272
+ @pinned_files.delete(normalized)
273
+ removed << pattern
274
+ end
275
+ else
276
+ files.each do |f|
277
+ if @pinned_files.include?(f)
278
+ @pinned_files.delete(f)
279
+ removed << f
280
+ end
281
+ end
282
+ end
283
+
284
+ if removed.any?
285
+ {
286
+ success: true,
287
+ message: "Unpinned #{removed.size} file(s): #{removed.join(", ")}",
288
+ action: :update_constraints,
289
+ data: {unpinned: removed}
290
+ }
291
+ else
292
+ {success: false, message: "No matching pinned files found", action: :none}
293
+ end
294
+ end
295
+
296
+ # Command: /focus <dir|glob>
297
+ def cmd_focus(args)
298
+ return {success: false, message: "Usage: /focus <dir|glob>", action: :none} if args.empty?
299
+
300
+ pattern = args.join(" ")
301
+ @focus_patterns << pattern
302
+
303
+ {
304
+ success: true,
305
+ message: "Focus set to: #{pattern}",
306
+ action: :update_constraints,
307
+ data: {focus: pattern}
308
+ }
309
+ end
310
+
311
+ # Command: /unfocus
312
+ def cmd_unfocus(args)
313
+ @focus_patterns.clear
314
+
315
+ {
316
+ success: true,
317
+ message: "Focus removed - all files in scope",
318
+ action: :update_constraints
319
+ }
320
+ end
321
+
322
+ # Command: /split
323
+ def cmd_split(args)
324
+ @split_mode = true
325
+
326
+ {
327
+ success: true,
328
+ message: "Split mode enabled - work will be divided into smaller contracts",
329
+ action: :split_work,
330
+ data: {split_mode: true}
331
+ }
332
+ end
333
+
334
+ # Command: /halt-on <pattern>
335
+ def cmd_halt_on(args)
336
+ return {success: false, message: "Usage: /halt-on <pattern>", action: :none} if args.empty?
337
+
338
+ pattern = args.join(" ").gsub(/^['"]|['"]$/, "") # Remove quotes
339
+ @halt_patterns << pattern
340
+
341
+ {
342
+ success: true,
343
+ message: "Will halt on test failures matching: #{pattern}",
344
+ action: :update_constraints,
345
+ data: {halt_pattern: pattern}
346
+ }
347
+ end
348
+
349
+ # Command: /unhalt <pattern>
350
+ def cmd_unhalt(args)
351
+ if args.empty?
352
+ @halt_patterns.clear
353
+ message = "All halt patterns removed"
354
+ else
355
+ pattern = args.join(" ").gsub(/^['"]|['"]$/, "")
356
+ if @halt_patterns.delete(pattern)
357
+ message = "Removed halt pattern: #{pattern}"
358
+ else
359
+ return {success: false, message: "Halt pattern not found: #{pattern}", action: :none}
360
+ end
361
+ end
362
+
363
+ {
364
+ success: true,
365
+ message: message,
366
+ action: :update_constraints
367
+ }
368
+ end
369
+
370
+ # Command: /status
371
+ def cmd_status(args)
372
+ lines = []
373
+ lines << "REPL Macro Status:"
374
+ lines << ""
375
+
376
+ if @pinned_files.any?
377
+ lines << "Pinned Files (#{@pinned_files.size}):"
378
+ @pinned_files.to_a.sort.each { |f| lines << " - #{f}" }
379
+ else
380
+ lines << "Pinned Files: (none)"
381
+ end
382
+
383
+ lines << ""
384
+
385
+ if @focus_patterns.any?
386
+ lines << "Focus Patterns (#{@focus_patterns.size}):"
387
+ @focus_patterns.each { |p| lines << " - #{p}" }
388
+ else
389
+ lines << "Focus: All files in scope"
390
+ end
391
+
392
+ lines << ""
393
+
394
+ if @halt_patterns.any?
395
+ lines << "Halt Patterns (#{@halt_patterns.size}):"
396
+ @halt_patterns.each { |p| lines << " - #{p}" }
397
+ else
398
+ lines << "Halt Patterns: (none)"
399
+ end
400
+
401
+ lines << ""
402
+ lines << "Split Mode: #{@split_mode ? "enabled" : "disabled"}"
403
+
404
+ {
405
+ success: true,
406
+ message: lines.join("\n"),
407
+ action: :display
408
+ }
409
+ end
410
+
411
+ # Command: /reset
412
+ def cmd_reset(args)
413
+ reset!
414
+
415
+ {
416
+ success: true,
417
+ message: "All REPL macros cleared",
418
+ action: :update_constraints
419
+ }
420
+ end
421
+
422
+ # Command: /help
423
+ def cmd_help(args)
424
+ message = if args.empty?
425
+ help_text = ["Available REPL Commands:", ""]
426
+ @commands.each do |cmd, info|
427
+ help_text << "#{cmd} - #{info[:description]}"
428
+ end
429
+ help_text << ""
430
+ help_text << "Type /help <command> for detailed help"
431
+ help_text.join("\n")
432
+ else
433
+ help(args.first)
434
+ end
435
+
436
+ {
437
+ success: true,
438
+ message: message,
439
+ action: :display
440
+ }
441
+ end
442
+
443
+ # Normalize file path
444
+ def normalize_path(path)
445
+ path.to_s.strip.gsub(%r{^./}, "")
446
+ end
447
+
448
+ # Expand glob pattern to actual files
449
+ def expand_pattern(pattern)
450
+ return [] unless pattern.include?("*") || pattern.include?("?")
451
+
452
+ Dir.glob(pattern, File::FNM_DOTMATCH).map { |f| normalize_path(f) }.reject { |f| File.directory?(f) }
453
+ end
454
+
455
+ # Check if path matches glob pattern
456
+ # Uses File.fnmatch for safe, efficient pattern matching without ReDoS risk
457
+ def matches_pattern?(path, pattern)
458
+ # Ruby's File.fnmatch with FNM_EXTGLOB handles most patterns safely
459
+ # For ** patterns, we need to handle them specially as fnmatch doesn't support ** natively
460
+
461
+ if pattern.include?("**")
462
+ # Convert ** to * for fnmatch compatibility and check if path contains the pattern parts
463
+ # Pattern like "lib/**/*.rb" should match "lib/foo/bar.rb"
464
+ pattern_parts = pattern.split("**").map(&:strip).reject(&:empty?)
465
+
466
+ if pattern_parts.empty?
467
+ # Pattern is just "**" - matches everything
468
+ true
469
+ elsif pattern_parts.size == 1
470
+ # Pattern like "**/file.rb" or "lib/**"
471
+ part = pattern_parts[0].sub(%r{^/}, "").sub(%r{/$}, "")
472
+ if pattern.start_with?("**")
473
+ # Matches if any part of the path matches
474
+ File.fnmatch(part, path, File::FNM_EXTGLOB) ||
475
+ File.fnmatch("**/#{part}", path, File::FNM_EXTGLOB) ||
476
+ path.end_with?(part) ||
477
+ path.include?("/#{part}")
478
+ else
479
+ # Pattern ends with **: match prefix
480
+ path.start_with?(part)
481
+ end
482
+ else
483
+ # Pattern like "lib/**/*.rb" - has prefix and suffix
484
+ prefix = pattern_parts[0].sub(%r{/$}, "")
485
+ suffix = pattern_parts[1].sub(%r{^/}, "")
486
+
487
+ path.start_with?(prefix) && File.fnmatch(suffix, path.sub(/^#{Regexp.escape(prefix)}\//, ""), File::FNM_EXTGLOB)
488
+ end
489
+ else
490
+ # Standard glob pattern - use File.fnmatch which is safe from ReDoS
491
+ # FNM_DOTMATCH allows * to match files starting with .
492
+ File.fnmatch(pattern, path, File::FNM_EXTGLOB | File::FNM_DOTMATCH)
493
+ end
494
+ end
495
+
496
+ # Count active constraints
497
+ def active_constraints_count
498
+ count = 0
499
+ count += @pinned_files.size
500
+ count += @focus_patterns.size
501
+ count += @halt_patterns.size
502
+ count += 1 if @split_mode
503
+ count
504
+ end
505
+
506
+ # Command: /pause
507
+ def cmd_pause(args)
508
+ {
509
+ success: true,
510
+ message: "Pause signal sent to work loop",
511
+ action: :pause_work_loop
512
+ }
513
+ end
514
+
515
+ # Command: /resume
516
+ def cmd_resume(args)
517
+ {
518
+ success: true,
519
+ message: "Resume signal sent to work loop",
520
+ action: :resume_work_loop
521
+ }
522
+ end
523
+
524
+ # Command: /cancel
525
+ def cmd_cancel(args)
526
+ save_checkpoint = !args.include?("--no-checkpoint")
527
+
528
+ {
529
+ success: true,
530
+ message: save_checkpoint ? "Cancelling with checkpoint save..." : "Cancelling without checkpoint...",
531
+ action: :cancel_work_loop,
532
+ data: {save_checkpoint: save_checkpoint}
533
+ }
534
+ end
535
+
536
+ # Command: /inject <instruction>
537
+ def cmd_inject(args)
538
+ return {success: false, message: "Usage: /inject <instruction> [--priority high|normal|low]", action: :none} if args.empty?
539
+
540
+ # Extract priority flag if present
541
+ priority = :normal
542
+ if (idx = args.index("--priority"))
543
+ priority = args[idx + 1]&.to_sym || :normal
544
+ args.delete_at(idx) # Remove --priority
545
+ args.delete_at(idx) # Remove priority value
546
+ end
547
+
548
+ instruction = args.join(" ")
549
+
550
+ {
551
+ success: true,
552
+ message: "Instruction queued for next iteration (priority: #{priority})",
553
+ action: :enqueue_instruction,
554
+ data: {
555
+ instruction: instruction,
556
+ type: :user_input,
557
+ priority: priority
558
+ }
559
+ }
560
+ end
561
+
562
+ # Command: /merge <plan_update>
563
+ def cmd_merge(args)
564
+ return {success: false, message: "Usage: /merge <plan_update>", action: :none} if args.empty?
565
+
566
+ plan_update = args.join(" ")
567
+
568
+ {
569
+ success: true,
570
+ message: "Plan update queued for next iteration",
571
+ action: :enqueue_instruction,
572
+ data: {
573
+ instruction: plan_update,
574
+ type: :plan_update,
575
+ priority: :high
576
+ }
577
+ }
578
+ end
579
+
580
+ # Command: /update guard <key>=<value>
581
+ def cmd_update(args)
582
+ return {success: false, message: "Usage: /update guard <key>=<value>", action: :none} if args.size < 2
583
+
584
+ category = args[0]
585
+ return {success: false, message: "Only 'guard' updates supported currently", action: :none} unless category == "guard"
586
+
587
+ key_value = args[1]
588
+ return {success: false, message: "Invalid format. Use: key=value", action: :none} unless key_value.include?("=")
589
+
590
+ key, value = key_value.split("=", 2)
591
+
592
+ {
593
+ success: true,
594
+ message: "Guard update queued: #{key} = #{value}",
595
+ action: :update_guard,
596
+ data: {key: key, value: value}
597
+ }
598
+ end
599
+
600
+ # Command: /reload config
601
+ def cmd_reload(args)
602
+ return {success: false, message: "Usage: /reload config", action: :none} if args.empty?
603
+
604
+ category = args[0]
605
+ return {success: false, message: "Only 'config' reload supported", action: :none} unless category == "config"
606
+
607
+ {
608
+ success: true,
609
+ message: "Configuration reload requested for next iteration",
610
+ action: :reload_config
611
+ }
612
+ end
613
+
614
+ # Command: /rollback <n>
615
+ def cmd_rollback(args)
616
+ return {success: false, message: "Usage: /rollback <n>", action: :none} if args.empty?
617
+
618
+ n = args[0].to_i
619
+ return {success: false, message: "Invalid number: #{args[0]}", action: :none} if n <= 0
620
+
621
+ {
622
+ success: true,
623
+ message: "Rollback #{n} commit(s) requested - will execute at next safe point",
624
+ action: :rollback_commits,
625
+ data: {count: n}
626
+ }
627
+ end
628
+
629
+ # Command: /undo last
630
+ def cmd_undo(args)
631
+ return {success: false, message: "Usage: /undo last", action: :none} unless args[0] == "last"
632
+
633
+ {
634
+ success: true,
635
+ message: "Undo last commit requested - will execute at next safe point",
636
+ action: :rollback_commits,
637
+ data: {count: 1}
638
+ }
639
+ end
640
+
641
+ # Command: /background
642
+ def cmd_background(args)
643
+ {
644
+ success: true,
645
+ message: "Detaching REPL and entering background daemon mode...",
646
+ action: :enter_background_mode
647
+ }
648
+ end
649
+ end
650
+ end
651
+ end
@@ -124,6 +124,14 @@ module Aidp
124
124
  "outs" => ["implementation_log.md"],
125
125
  "gate" => false,
126
126
  "implementation" => true # Special step that runs development tasks
127
+ },
128
+ # Simple task execution - for one-off commands and simple fixes
129
+ "99_SIMPLE_TASK" => {
130
+ "templates" => ["implementation/simple_task.md"],
131
+ "description" => "Execute Simple Task (one-off commands, quick fixes, linting)",
132
+ "outs" => [],
133
+ "gate" => false,
134
+ "simple" => true # Special step for simple, focused tasks
127
135
  }
128
136
  }.freeze
129
137
  end