aidp 0.15.2 → 0.17.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +47 -0
  3. data/lib/aidp/analyze/error_handler.rb +46 -28
  4. data/lib/aidp/analyze/progress.rb +1 -1
  5. data/lib/aidp/analyze/runner.rb +27 -5
  6. data/lib/aidp/analyze/steps.rb +4 -0
  7. data/lib/aidp/cli/jobs_command.rb +2 -1
  8. data/lib/aidp/cli.rb +1086 -4
  9. data/lib/aidp/concurrency/backoff.rb +148 -0
  10. data/lib/aidp/concurrency/exec.rb +192 -0
  11. data/lib/aidp/concurrency/wait.rb +148 -0
  12. data/lib/aidp/concurrency.rb +71 -0
  13. data/lib/aidp/config.rb +21 -1
  14. data/lib/aidp/daemon/runner.rb +9 -8
  15. data/lib/aidp/debug_mixin.rb +1 -0
  16. data/lib/aidp/errors.rb +12 -0
  17. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  18. data/lib/aidp/execute/checkpoint.rb +1 -1
  19. data/lib/aidp/execute/future_work_backlog.rb +1 -1
  20. data/lib/aidp/execute/interactive_repl.rb +102 -11
  21. data/lib/aidp/execute/progress.rb +1 -1
  22. data/lib/aidp/execute/repl_macros.rb +845 -2
  23. data/lib/aidp/execute/runner.rb +27 -5
  24. data/lib/aidp/execute/steps.rb +2 -0
  25. data/lib/aidp/harness/config_loader.rb +24 -2
  26. data/lib/aidp/harness/config_validator.rb +1 -1
  27. data/lib/aidp/harness/enhanced_runner.rb +16 -2
  28. data/lib/aidp/harness/error_handler.rb +1 -1
  29. data/lib/aidp/harness/provider_info.rb +19 -15
  30. data/lib/aidp/harness/provider_manager.rb +47 -41
  31. data/lib/aidp/harness/runner.rb +3 -11
  32. data/lib/aidp/harness/state/persistence.rb +1 -6
  33. data/lib/aidp/harness/state_manager.rb +115 -7
  34. data/lib/aidp/harness/status_display.rb +11 -18
  35. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  36. data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
  37. data/lib/aidp/harness/user_interface.rb +12 -15
  38. data/lib/aidp/jobs/background_runner.rb +16 -6
  39. data/lib/aidp/providers/codex.rb +0 -1
  40. data/lib/aidp/providers/cursor.rb +0 -1
  41. data/lib/aidp/providers/github_copilot.rb +0 -1
  42. data/lib/aidp/providers/opencode.rb +0 -1
  43. data/lib/aidp/skills/composer.rb +178 -0
  44. data/lib/aidp/skills/loader.rb +205 -0
  45. data/lib/aidp/skills/registry.rb +222 -0
  46. data/lib/aidp/skills/router.rb +178 -0
  47. data/lib/aidp/skills/skill.rb +174 -0
  48. data/lib/aidp/skills/wizard/builder.rb +141 -0
  49. data/lib/aidp/skills/wizard/controller.rb +145 -0
  50. data/lib/aidp/skills/wizard/differ.rb +232 -0
  51. data/lib/aidp/skills/wizard/prompter.rb +317 -0
  52. data/lib/aidp/skills/wizard/template_library.rb +164 -0
  53. data/lib/aidp/skills/wizard/writer.rb +105 -0
  54. data/lib/aidp/skills.rb +30 -0
  55. data/lib/aidp/version.rb +1 -1
  56. data/lib/aidp/watch/build_processor.rb +93 -28
  57. data/lib/aidp/watch/runner.rb +3 -2
  58. data/lib/aidp/workstream_executor.rb +244 -0
  59. data/lib/aidp/workstream_state.rb +212 -0
  60. data/lib/aidp/worktree.rb +208 -0
  61. data/lib/aidp.rb +6 -0
  62. data/templates/skills/README.md +334 -0
  63. data/templates/skills/architecture_analyst/SKILL.md +173 -0
  64. data/templates/skills/product_strategist/SKILL.md +141 -0
  65. data/templates/skills/repository_analyst/SKILL.md +117 -0
  66. data/templates/skills/test_analyzer/SKILL.md +213 -0
  67. metadata +29 -4
@@ -9,13 +9,16 @@ module Aidp
9
9
  # - /split - Divide work into smaller contracts
10
10
  # - /halt-on <pattern> - Pause on specific test failures
11
11
  class ReplMacros
12
- attr_reader :pinned_files, :focus_patterns, :halt_patterns, :split_mode
12
+ attr_reader :pinned_files, :focus_patterns, :halt_patterns, :split_mode, :current_workstream, :current_skill
13
13
 
14
- def initialize
14
+ def initialize(project_dir: Dir.pwd)
15
15
  @pinned_files = Set.new
16
16
  @focus_patterns = []
17
17
  @halt_patterns = []
18
18
  @split_mode = false
19
+ @project_dir = project_dir
20
+ @current_workstream = nil
21
+ @current_skill = nil
19
22
  @commands = register_commands
20
23
  end
21
24
 
@@ -62,6 +65,8 @@ module Aidp
62
65
  focus_patterns: @focus_patterns,
63
66
  halt_patterns: @halt_patterns,
64
67
  split_mode: @split_mode,
68
+ current_workstream: @current_workstream,
69
+ current_skill: @current_skill,
65
70
  active_constraints: active_constraints_count
66
71
  }
67
72
  end
@@ -91,6 +96,49 @@ module Aidp
91
96
  end
92
97
  end
93
98
 
99
+ # Get current workstream path (or project_dir if none)
100
+ def current_workstream_path
101
+ return @project_dir unless @current_workstream
102
+
103
+ require_relative "../worktree"
104
+ ws = Aidp::Worktree.info(slug: @current_workstream, project_dir: @project_dir)
105
+ ws ? ws[:path] : @project_dir
106
+ end
107
+
108
+ # Switch to a workstream (called by external code)
109
+ def switch_workstream(slug)
110
+ require_relative "../worktree"
111
+ ws = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
112
+ return false unless ws
113
+
114
+ @current_workstream = slug
115
+ true
116
+ end
117
+
118
+ # Retrieve the current skill object, or nil if none is selected
119
+ #
120
+ # This method provides access to the full skill object (with content, providers, etc.)
121
+ # for the currently selected skill via `/skill use <id>`.
122
+ #
123
+ # @return [Aidp::Skills::Skill, nil] The current skill object or nil
124
+ #
125
+ # @example
126
+ # repl = ReplMacros.new(project_dir: Dir.pwd)
127
+ # repl.execute("/skill use repository_analyst")
128
+ # skill = repl.current_skill_object
129
+ # puts skill.content if skill # => skill's markdown content
130
+ def current_skill_object
131
+ return nil unless @current_skill
132
+
133
+ require_relative "../skills"
134
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir)
135
+ registry.load_skills
136
+ registry.find(@current_skill)
137
+ rescue => e
138
+ Aidp.log_error("repl_macros", "Failed to load current skill object", error: e.message)
139
+ nil
140
+ end
141
+
94
142
  private
95
143
 
96
144
  # Register all available REPL commands
@@ -215,6 +263,18 @@ module Aidp
215
263
  usage: "/help [command]",
216
264
  example: "/help pin",
217
265
  handler: method(:cmd_help)
266
+ },
267
+ "/ws" => {
268
+ description: "Manage workstreams (parallel work contexts)",
269
+ usage: "/ws <list|new|rm|switch|status> [args]",
270
+ example: "/ws list",
271
+ handler: method(:cmd_ws)
272
+ },
273
+ "/skill" => {
274
+ description: "Manage and view skills (agent personas)",
275
+ usage: "/skill <list|show|use> [args]",
276
+ example: "/skill list",
277
+ handler: method(:cmd_skill)
218
278
  }
219
279
  }
220
280
  end
@@ -373,6 +433,23 @@ module Aidp
373
433
  lines << "REPL Macro Status:"
374
434
  lines << ""
375
435
 
436
+ # Show current workstream
437
+ if @current_workstream
438
+ require_relative "../worktree"
439
+ ws = Aidp::Worktree.info(slug: @current_workstream, project_dir: @project_dir)
440
+ if ws
441
+ lines << "Current Workstream: #{@current_workstream}"
442
+ lines << " Path: #{ws[:path]}"
443
+ lines << " Branch: #{ws[:branch]}"
444
+ else
445
+ lines << "Current Workstream: #{@current_workstream} (not found)"
446
+ end
447
+ else
448
+ lines << "Current Workstream: (none - using main project)"
449
+ end
450
+
451
+ lines << ""
452
+
376
453
  if @pinned_files.any?
377
454
  lines << "Pinned Files (#{@pinned_files.size}):"
378
455
  @pinned_files.to_a.sort.each { |f| lines << " - #{f}" }
@@ -646,6 +723,772 @@ module Aidp
646
723
  action: :enter_background_mode
647
724
  }
648
725
  end
726
+
727
+ # Command: /ws <subcommand> [args]
728
+ def cmd_ws(args)
729
+ require_relative "../worktree"
730
+
731
+ subcommand = args.shift
732
+
733
+ case subcommand
734
+ when "list", nil
735
+ # List all workstreams
736
+ workstreams = Aidp::Worktree.list(project_dir: @project_dir)
737
+
738
+ if workstreams.empty?
739
+ return {
740
+ success: true,
741
+ message: "No workstreams found.\nCreate one with: /ws new <slug>",
742
+ action: :display
743
+ }
744
+ end
745
+
746
+ require_relative "../workstream_state"
747
+ lines = ["Workstreams:"]
748
+ workstreams.each do |ws|
749
+ status = ws[:active] ? "✓" : "✗"
750
+ current = (@current_workstream == ws[:slug]) ? " [CURRENT]" : ""
751
+ state = Aidp::WorkstreamState.read(slug: ws[:slug], project_dir: @project_dir) || {}
752
+ iter = state[:iterations] || 0
753
+ task = state[:task] ? state[:task][0, 50] : ""
754
+ lines << " #{status} #{ws[:slug]} (#{ws[:branch]}) iter=#{iter}#{current}#{" task=" + task unless task.empty?}"
755
+ end
756
+
757
+ {
758
+ success: true,
759
+ message: lines.join("\n"),
760
+ action: :display
761
+ }
762
+
763
+ when "new"
764
+ # Create new workstream
765
+ slug = args.shift
766
+ unless slug
767
+ return {
768
+ success: false,
769
+ message: "Usage: /ws new <slug> [--base-branch <branch>]",
770
+ action: :none
771
+ }
772
+ end
773
+
774
+ # Validate slug format
775
+ unless slug.match?(/^[a-z0-9]+(-[a-z0-9]+)*$/)
776
+ return {
777
+ success: false,
778
+ message: "Invalid slug format. Must be lowercase with hyphens (e.g., 'issue-123')",
779
+ action: :none
780
+ }
781
+ end
782
+
783
+ # Parse options and task description
784
+ base_branch = nil
785
+ task_parts = []
786
+ until args.empty?
787
+ token = args.shift
788
+ if token == "--base-branch"
789
+ base_branch = args.shift
790
+ else
791
+ task_parts << token
792
+ end
793
+ end
794
+ task = task_parts.join(" ")
795
+
796
+ begin
797
+ result = Aidp::Worktree.create(
798
+ slug: slug,
799
+ project_dir: @project_dir,
800
+ base_branch: base_branch,
801
+ task: (task unless task.empty?)
802
+ )
803
+
804
+ msg_lines = []
805
+ msg_lines << "\u2713 Created workstream: #{slug}"
806
+ msg_lines << " Path: #{result[:path]}"
807
+ msg_lines << " Branch: #{result[:branch]}"
808
+ msg_lines << " Task: #{task}" unless task.empty?
809
+ msg_lines << ""
810
+ msg_lines << "Switch to it with: /ws switch #{slug}"
811
+
812
+ {
813
+ success: true,
814
+ message: msg_lines.join("\n"),
815
+ action: :display
816
+ }
817
+ rescue Aidp::Worktree::Error => e
818
+ {
819
+ success: false,
820
+ message: "Failed to create workstream: #{e.message}",
821
+ action: :none
822
+ }
823
+ end
824
+
825
+ when "switch"
826
+ # Switch to workstream
827
+ slug = args.shift
828
+ unless slug
829
+ return {
830
+ success: false,
831
+ message: "Usage: /ws switch <slug>",
832
+ action: :none
833
+ }
834
+ end
835
+
836
+ begin
837
+ ws = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
838
+ unless ws
839
+ return {
840
+ success: false,
841
+ message: "Workstream not found: #{slug}",
842
+ action: :none
843
+ }
844
+ end
845
+
846
+ @current_workstream = slug
847
+
848
+ {
849
+ success: true,
850
+ message: "✓ Switched to workstream: #{slug}\n All operations will now use: #{ws[:path]}",
851
+ action: :switch_workstream,
852
+ data: {slug: slug, path: ws[:path], branch: ws[:branch]}
853
+ }
854
+ rescue Aidp::Worktree::Error => e
855
+ {
856
+ success: false,
857
+ message: "Failed to switch workstream: #{e.message}",
858
+ action: :none
859
+ }
860
+ end
861
+
862
+ when "rm"
863
+ # Remove workstream
864
+ slug = args.shift
865
+ unless slug
866
+ return {
867
+ success: false,
868
+ message: "Usage: /ws rm <slug> [--delete-branch]",
869
+ action: :none
870
+ }
871
+ end
872
+
873
+ delete_branch = args.include?("--delete-branch")
874
+
875
+ # Don't allow removing current workstream
876
+ if @current_workstream == slug
877
+ return {
878
+ success: false,
879
+ message: "Cannot remove current workstream. Switch to another first.",
880
+ action: :none
881
+ }
882
+ end
883
+
884
+ begin
885
+ Aidp::Worktree.remove(
886
+ slug: slug,
887
+ project_dir: @project_dir,
888
+ delete_branch: delete_branch
889
+ )
890
+
891
+ {
892
+ success: true,
893
+ message: "✓ Removed workstream: #{slug}#{" (branch deleted)" if delete_branch}",
894
+ action: :display
895
+ }
896
+ rescue Aidp::Worktree::Error => e
897
+ {
898
+ success: false,
899
+ message: "Failed to remove workstream: #{e.message}",
900
+ action: :none
901
+ }
902
+ end
903
+
904
+ when "status"
905
+ # Show workstream status
906
+ slug = args.shift || @current_workstream
907
+
908
+ unless slug
909
+ return {
910
+ success: false,
911
+ message: "Usage: /ws status [slug]\nNo current workstream set. Specify a slug or use /ws switch first.",
912
+ action: :none
913
+ }
914
+ end
915
+
916
+ begin
917
+ ws = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
918
+ unless ws
919
+ return {
920
+ success: false,
921
+ message: "Workstream not found: #{slug}",
922
+ action: :none
923
+ }
924
+ end
925
+
926
+ require_relative "../workstream_state"
927
+ state = Aidp::WorkstreamState.read(slug: slug, project_dir: @project_dir) || {}
928
+ iter = state[:iterations] || 0
929
+ task = state[:task]
930
+ elapsed = Aidp::WorkstreamState.elapsed_seconds(slug: slug, project_dir: @project_dir)
931
+ events = Aidp::WorkstreamState.recent_events(slug: slug, project_dir: @project_dir, limit: 5)
932
+
933
+ lines = []
934
+ lines << "Workstream: #{slug}#{" [CURRENT]" if @current_workstream == slug}"
935
+ lines << " Path: #{ws[:path]}"
936
+ lines << " Branch: #{ws[:branch]}"
937
+ lines << " Created: #{Time.parse(ws[:created_at]).strftime("%Y-%m-%d %H:%M:%S")}"
938
+ lines << " Status: #{ws[:active] ? "Active" : "Inactive"}"
939
+ lines << " Iterations: #{iter}"
940
+ lines << " Elapsed: #{elapsed}s"
941
+ lines << " Task: #{task}" if task
942
+ if events.any?
943
+ lines << " Recent Events:"
944
+ events.each do |ev|
945
+ lines << " - #{ev[:timestamp]} #{ev[:type]} #{ev[:data].inspect if ev[:data]}"
946
+ end
947
+ end
948
+
949
+ {
950
+ success: true,
951
+ message: lines.join("\n"),
952
+ action: :display
953
+ }
954
+ rescue Aidp::Worktree::Error => e
955
+ {
956
+ success: false,
957
+ message: "Failed to get workstream status: #{e.message}",
958
+ action: :none
959
+ }
960
+ end
961
+
962
+ when "pause"
963
+ # Pause workstream
964
+ slug = args.shift || @current_workstream
965
+
966
+ unless slug
967
+ return {
968
+ success: false,
969
+ message: "Usage: /ws pause [slug]\nNo current workstream set. Specify a slug or use /ws switch first.",
970
+ action: :none
971
+ }
972
+ end
973
+
974
+ require_relative "../workstream_state"
975
+ result = Aidp::WorkstreamState.pause(slug: slug, project_dir: @project_dir)
976
+ if result[:error]
977
+ {
978
+ success: false,
979
+ message: "Failed to pause: #{result[:error]}",
980
+ action: :none
981
+ }
982
+ else
983
+ {
984
+ success: true,
985
+ message: "⏸️ Paused workstream: #{slug}",
986
+ action: :display
987
+ }
988
+ end
989
+
990
+ when "resume"
991
+ # Resume workstream
992
+ slug = args.shift || @current_workstream
993
+
994
+ unless slug
995
+ return {
996
+ success: false,
997
+ message: "Usage: /ws resume [slug]\nNo current workstream set. Specify a slug or use /ws switch first.",
998
+ action: :none
999
+ }
1000
+ end
1001
+
1002
+ require_relative "../workstream_state"
1003
+ result = Aidp::WorkstreamState.resume(slug: slug, project_dir: @project_dir)
1004
+ if result[:error]
1005
+ {
1006
+ success: false,
1007
+ message: "Failed to resume: #{result[:error]}",
1008
+ action: :none
1009
+ }
1010
+ else
1011
+ {
1012
+ success: true,
1013
+ message: "▶️ Resumed workstream: #{slug}",
1014
+ action: :display
1015
+ }
1016
+ end
1017
+
1018
+ when "complete"
1019
+ # Mark workstream as completed
1020
+ slug = args.shift || @current_workstream
1021
+
1022
+ unless slug
1023
+ return {
1024
+ success: false,
1025
+ message: "Usage: /ws complete [slug]\nNo current workstream set. Specify a slug or use /ws switch first.",
1026
+ action: :none
1027
+ }
1028
+ end
1029
+
1030
+ require_relative "../workstream_state"
1031
+ result = Aidp::WorkstreamState.complete(slug: slug, project_dir: @project_dir)
1032
+ if result[:error]
1033
+ {
1034
+ success: false,
1035
+ message: "Failed to complete: #{result[:error]}",
1036
+ action: :none
1037
+ }
1038
+ else
1039
+ {
1040
+ success: true,
1041
+ message: "✅ Completed workstream: #{slug}",
1042
+ action: :display
1043
+ }
1044
+ end
1045
+
1046
+ when "dashboard"
1047
+ # Show multi-workstream dashboard
1048
+ workstreams = Aidp::Worktree.list(project_dir: @project_dir)
1049
+
1050
+ if workstreams.empty?
1051
+ return {
1052
+ success: true,
1053
+ message: "No workstreams found.\nCreate one with: /ws new <slug>",
1054
+ action: :display
1055
+ }
1056
+ end
1057
+
1058
+ require_relative "../workstream_state"
1059
+
1060
+ lines = ["Workstreams Dashboard", "=" * 80, ""]
1061
+
1062
+ # Aggregate state from all workstreams
1063
+ workstreams.each do |ws|
1064
+ state = Aidp::WorkstreamState.read(slug: ws[:slug], project_dir: @project_dir) || {}
1065
+ status = state[:status] || "active"
1066
+ iterations = state[:iterations] || 0
1067
+ elapsed = Aidp::WorkstreamState.elapsed_seconds(slug: ws[:slug], project_dir: @project_dir)
1068
+ task = state[:task] && state[:task].to_s[0, 40]
1069
+ recent_events = Aidp::WorkstreamState.recent_events(slug: ws[:slug], project_dir: @project_dir, limit: 1)
1070
+ recent_event = recent_events.first
1071
+
1072
+ status_icon = case status
1073
+ when "active" then "▶️"
1074
+ when "paused" then "⏸️"
1075
+ when "completed" then "✅"
1076
+ when "removed" then "❌"
1077
+ else "?"
1078
+ end
1079
+
1080
+ current = (@current_workstream == ws[:slug]) ? " [CURRENT]" : ""
1081
+ lines << "#{status_icon} #{ws[:slug]}#{current}"
1082
+ lines << " Status: #{status} | Iterations: #{iterations} | Elapsed: #{elapsed}s"
1083
+ lines << " Task: #{task}" if task
1084
+ if recent_event
1085
+ event_time = Time.parse(recent_event[:timestamp]).strftime("%Y-%m-%d %H:%M")
1086
+ lines << " Recent: #{recent_event[:type]} at #{event_time}"
1087
+ end
1088
+ lines << ""
1089
+ end
1090
+
1091
+ # Summary
1092
+ status_counts = workstreams.group_by do |ws|
1093
+ state = Aidp::WorkstreamState.read(slug: ws[:slug], project_dir: @project_dir) || {}
1094
+ state[:status] || "active"
1095
+ end
1096
+ summary_parts = status_counts.map { |status, ws_list| "#{status}: #{ws_list.size}" }
1097
+ lines << "Summary: #{summary_parts.join(", ")}"
1098
+
1099
+ {
1100
+ success: true,
1101
+ message: lines.join("\n"),
1102
+ action: :display
1103
+ }
1104
+
1105
+ when "pause-all"
1106
+ # Pause all active workstreams
1107
+ workstreams = Aidp::Worktree.list(project_dir: @project_dir)
1108
+ require_relative "../workstream_state"
1109
+ paused_count = 0
1110
+ workstreams.each do |ws|
1111
+ state = Aidp::WorkstreamState.read(slug: ws[:slug], project_dir: @project_dir)
1112
+ next unless state && state[:status] == "active"
1113
+ result = Aidp::WorkstreamState.pause(slug: ws[:slug], project_dir: @project_dir)
1114
+ paused_count += 1 unless result[:error]
1115
+ end
1116
+ {
1117
+ success: true,
1118
+ message: "⏸️ Paused #{paused_count} workstream(s)",
1119
+ action: :display
1120
+ }
1121
+
1122
+ when "resume-all"
1123
+ # Resume all paused workstreams
1124
+ workstreams = Aidp::Worktree.list(project_dir: @project_dir)
1125
+ require_relative "../workstream_state"
1126
+ resumed_count = 0
1127
+ workstreams.each do |ws|
1128
+ state = Aidp::WorkstreamState.read(slug: ws[:slug], project_dir: @project_dir)
1129
+ next unless state && state[:status] == "paused"
1130
+ result = Aidp::WorkstreamState.resume(slug: ws[:slug], project_dir: @project_dir)
1131
+ resumed_count += 1 unless result[:error]
1132
+ end
1133
+ {
1134
+ success: true,
1135
+ message: "▶️ Resumed #{resumed_count} workstream(s)",
1136
+ action: :display
1137
+ }
1138
+
1139
+ when "stop-all"
1140
+ # Complete all active workstreams
1141
+ workstreams = Aidp::Worktree.list(project_dir: @project_dir)
1142
+ require_relative "../workstream_state"
1143
+ stopped_count = 0
1144
+ workstreams.each do |ws|
1145
+ state = Aidp::WorkstreamState.read(slug: ws[:slug], project_dir: @project_dir)
1146
+ next unless state && state[:status] == "active"
1147
+ result = Aidp::WorkstreamState.complete(slug: ws[:slug], project_dir: @project_dir)
1148
+ stopped_count += 1 unless result[:error]
1149
+ end
1150
+ {
1151
+ success: true,
1152
+ message: "⏹️ Stopped #{stopped_count} workstream(s)",
1153
+ action: :display
1154
+ }
1155
+
1156
+ when "run"
1157
+ # Run one or more workstreams in parallel
1158
+ require_relative "../workstream_executor"
1159
+
1160
+ slugs = []
1161
+ max_concurrent = 3
1162
+
1163
+ # Parse slugs from args
1164
+ args.each do |arg|
1165
+ next if arg.start_with?("--")
1166
+ slugs << arg
1167
+ end
1168
+
1169
+ if slugs.empty?
1170
+ return {
1171
+ success: false,
1172
+ message: "Usage: /ws run <slug1> [slug2...]\n\nExamples:\n /ws run issue-123\n /ws run issue-123 issue-456 feature-x",
1173
+ action: :none
1174
+ }
1175
+ end
1176
+
1177
+ begin
1178
+ executor = Aidp::WorkstreamExecutor.new(project_dir: @project_dir, max_concurrent: max_concurrent)
1179
+ results = executor.execute_parallel(slugs, {mode: :execute})
1180
+
1181
+ success_count = results.count { |r| r.status == "completed" }
1182
+ lines = ["🚀 Parallel Execution Results", "=" * 60, ""]
1183
+
1184
+ results.each do |result|
1185
+ icon = (result.status == "completed") ? "✅" : "❌"
1186
+ duration = "#{result.duration.round(1)}s"
1187
+ lines << "#{icon} #{result.slug}: #{result.status} (#{duration})"
1188
+ lines << " Error: #{result.error}" if result.error
1189
+ end
1190
+
1191
+ lines << ""
1192
+ lines << "Summary: #{success_count}/#{results.size} completed"
1193
+
1194
+ {
1195
+ success: success_count == results.size,
1196
+ message: lines.join("\n"),
1197
+ action: :display
1198
+ }
1199
+ rescue => e
1200
+ {
1201
+ success: false,
1202
+ message: "Parallel execution error: #{e.message}",
1203
+ action: :none
1204
+ }
1205
+ end
1206
+
1207
+ when "run-all"
1208
+ # Run all active workstreams in parallel
1209
+ require_relative "../workstream_executor"
1210
+
1211
+ max_concurrent = 3
1212
+
1213
+ begin
1214
+ executor = Aidp::WorkstreamExecutor.new(project_dir: @project_dir, max_concurrent: max_concurrent)
1215
+ results = executor.execute_all({mode: :execute})
1216
+
1217
+ if results.empty?
1218
+ return {
1219
+ success: true,
1220
+ message: "⚠️ No active workstreams to run",
1221
+ action: :display
1222
+ }
1223
+ end
1224
+
1225
+ success_count = results.count { |r| r.status == "completed" }
1226
+ lines = ["🚀 Parallel Execution Results (All Active)", "=" * 60, ""]
1227
+
1228
+ results.each do |result|
1229
+ icon = (result.status == "completed") ? "✅" : "❌"
1230
+ duration = "#{result.duration.round(1)}s"
1231
+ lines << "#{icon} #{result.slug}: #{result.status} (#{duration})"
1232
+ lines << " Error: #{result.error}" if result.error
1233
+ end
1234
+
1235
+ lines << ""
1236
+ lines << "Summary: #{success_count}/#{results.size} completed"
1237
+
1238
+ {
1239
+ success: success_count == results.size,
1240
+ message: lines.join("\n"),
1241
+ action: :display
1242
+ }
1243
+ rescue => e
1244
+ {
1245
+ success: false,
1246
+ message: "Parallel execution error: #{e.message}",
1247
+ action: :none
1248
+ }
1249
+ end
1250
+
1251
+ else
1252
+ {
1253
+ success: false,
1254
+ message: "Usage: /ws <command> [args]\n\nCommands:\n list - List all workstreams\n new <slug> - Create new workstream\n switch <slug> - Switch to workstream\n rm <slug> - Remove workstream\n status [slug] - Show workstream status\n run <slug...> - Run workstream(s) in parallel\n run-all - Run all active workstreams in parallel\n dashboard - Show multi-workstream overview\n pause [slug] - Pause workstream\n resume [slug] - Resume workstream\n complete [slug] - Mark workstream as completed\n pause-all - Pause all active workstreams\n resume-all - Resume all paused workstreams\n stop-all - Stop all active workstreams\n\nOptions:\n --base-branch <branch> - Branch to create from (for 'new')\n --delete-branch - Also delete git branch (for 'rm')\n\nExamples:\n /ws list\n /ws new issue-123\n /ws switch issue-123\n /ws run issue-123 # Run single workstream\n /ws run issue-123 issue-456 # Run multiple in parallel\n /ws run-all # Run all active workstreams\n /ws status\n /ws dashboard\n /ws pause-all\n /ws resume-all\n /ws stop-all\n /ws rm issue-123 --delete-branch",
1255
+ action: :none
1256
+ }
1257
+ end
1258
+ end
1259
+
1260
+ # Command: /skill <subcommand> [args]
1261
+ def cmd_skill(args)
1262
+ require_relative "../skills"
1263
+
1264
+ subcommand = args.shift
1265
+
1266
+ case subcommand
1267
+ when "list", nil
1268
+ # List all available skills
1269
+ begin
1270
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir)
1271
+ registry.load_skills
1272
+
1273
+ skills = registry.all
1274
+
1275
+ if skills.empty?
1276
+ return {
1277
+ success: true,
1278
+ message: "No skills found.\nCreate one in skills/ or .aidp/skills/",
1279
+ action: :display
1280
+ }
1281
+ end
1282
+
1283
+ lines = ["Available Skills:", ""]
1284
+ by_source = registry.by_source
1285
+
1286
+ if by_source[:template].any?
1287
+ lines << "Template Skills:"
1288
+ by_source[:template].each do |skill_id|
1289
+ skill = registry.find(skill_id)
1290
+ lines << " • #{skill_id} - #{skill.description}"
1291
+ end
1292
+ lines << ""
1293
+ end
1294
+
1295
+ if by_source[:project].any?
1296
+ lines << "Project Skills:"
1297
+ by_source[:project].each do |skill_id|
1298
+ skill = registry.find(skill_id)
1299
+ lines << " • #{skill_id} - #{skill.description} [PROJECT]"
1300
+ end
1301
+ lines << ""
1302
+ end
1303
+
1304
+ lines << "Use '/skill show <id>' for details or '/skill use <id>' to activate"
1305
+
1306
+ {
1307
+ success: true,
1308
+ message: lines.join("\n"),
1309
+ action: :display
1310
+ }
1311
+ rescue => e
1312
+ {
1313
+ success: false,
1314
+ message: "Failed to list skills: #{e.message}",
1315
+ action: :none
1316
+ }
1317
+ end
1318
+
1319
+ when "show"
1320
+ # Show detailed skill information
1321
+ skill_id = args.shift
1322
+
1323
+ unless skill_id
1324
+ return {
1325
+ success: false,
1326
+ message: "Usage: /skill show <skill-id>",
1327
+ action: :none
1328
+ }
1329
+ end
1330
+
1331
+ begin
1332
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir)
1333
+ registry.load_skills
1334
+
1335
+ skill = registry.find(skill_id)
1336
+
1337
+ unless skill
1338
+ return {
1339
+ success: false,
1340
+ message: "Skill not found: #{skill_id}\nUse '/skill list' to see available skills",
1341
+ action: :none
1342
+ }
1343
+ end
1344
+
1345
+ details = skill.details
1346
+ lines = []
1347
+ lines << "Skill: #{details[:name]} (#{details[:id]})"
1348
+ lines << "Version: #{details[:version]}"
1349
+ lines << "Source: #{details[:source]}"
1350
+ lines << ""
1351
+ lines << "Description:"
1352
+ lines << " #{details[:description]}"
1353
+ lines << ""
1354
+
1355
+ if details[:expertise].any?
1356
+ lines << "Expertise:"
1357
+ details[:expertise].each { |e| lines << " • #{e}" }
1358
+ lines << ""
1359
+ end
1360
+
1361
+ if details[:keywords].any?
1362
+ lines << "Keywords: #{details[:keywords].join(", ")}"
1363
+ lines << ""
1364
+ end
1365
+
1366
+ if details[:when_to_use].any?
1367
+ lines << "When to Use:"
1368
+ details[:when_to_use].each { |w| lines << " • #{w}" }
1369
+ lines << ""
1370
+ end
1371
+
1372
+ if details[:when_not_to_use].any?
1373
+ lines << "When NOT to Use:"
1374
+ details[:when_not_to_use].each { |w| lines << " • #{w}" }
1375
+ lines << ""
1376
+ end
1377
+
1378
+ lines << if details[:compatible_providers].any?
1379
+ "Compatible Providers: #{details[:compatible_providers].join(", ")}"
1380
+ else
1381
+ "Compatible Providers: all"
1382
+ end
1383
+
1384
+ {
1385
+ success: true,
1386
+ message: lines.join("\n"),
1387
+ action: :display
1388
+ }
1389
+ rescue => e
1390
+ {
1391
+ success: false,
1392
+ message: "Failed to show skill: #{e.message}",
1393
+ action: :none
1394
+ }
1395
+ end
1396
+
1397
+ when "search"
1398
+ # Search skills by query
1399
+ query = args.join(" ")
1400
+
1401
+ unless query && !query.empty?
1402
+ return {
1403
+ success: false,
1404
+ message: "Usage: /skill search <query>",
1405
+ action: :none
1406
+ }
1407
+ end
1408
+
1409
+ begin
1410
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir)
1411
+ registry.load_skills
1412
+
1413
+ matching_skills = registry.search(query)
1414
+
1415
+ if matching_skills.empty?
1416
+ return {
1417
+ success: true,
1418
+ message: "No skills found matching '#{query}'",
1419
+ action: :display
1420
+ }
1421
+ end
1422
+
1423
+ lines = ["Skills matching '#{query}':", ""]
1424
+ matching_skills.each do |skill|
1425
+ lines << " • #{skill.id} - #{skill.description}"
1426
+ end
1427
+
1428
+ {
1429
+ success: true,
1430
+ message: lines.join("\n"),
1431
+ action: :display
1432
+ }
1433
+ rescue => e
1434
+ {
1435
+ success: false,
1436
+ message: "Failed to search skills: #{e.message}",
1437
+ action: :none
1438
+ }
1439
+ end
1440
+
1441
+ when "use"
1442
+ # Switch to a specific skill
1443
+ skill_id = args.shift
1444
+
1445
+ unless skill_id
1446
+ return {
1447
+ success: false,
1448
+ message: "Usage: /skill use <skill-id>",
1449
+ action: :none
1450
+ }
1451
+ end
1452
+
1453
+ begin
1454
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir)
1455
+ registry.load_skills
1456
+
1457
+ skill = registry.find(skill_id)
1458
+
1459
+ unless skill
1460
+ return {
1461
+ success: false,
1462
+ message: "Skill not found: #{skill_id}\nUse '/skill list' to see available skills",
1463
+ action: :none
1464
+ }
1465
+ end
1466
+
1467
+ # Store the current skill for the session
1468
+ @current_skill = skill_id
1469
+
1470
+ {
1471
+ success: true,
1472
+ message: "✓ Now using skill: #{skill.name} (#{skill_id})\n\n#{skill.description}",
1473
+ action: :switch_skill,
1474
+ data: {skill_id: skill_id, skill: skill}
1475
+ }
1476
+ rescue => e
1477
+ {
1478
+ success: false,
1479
+ message: "Failed to switch skill: #{e.message}",
1480
+ action: :none
1481
+ }
1482
+ end
1483
+
1484
+ else
1485
+ {
1486
+ success: false,
1487
+ message: "Usage: /skill <command> [args]\n\nCommands:\n list - List all available skills\n show <id> - Show detailed skill information\n search <query> - Search skills by keyword\n use <id> - Switch to a specific skill\n\nExamples:\n /skill list\n /skill show repository_analyst\n /skill search git\n /skill use repository_analyst",
1488
+ action: :none
1489
+ }
1490
+ end
1491
+ end
649
1492
  end
650
1493
  end
651
1494
  end