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