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.
- checksums.yaml +4 -4
- data/README.md +47 -0
- data/lib/aidp/analyze/error_handler.rb +14 -15
- data/lib/aidp/analyze/runner.rb +27 -5
- data/lib/aidp/analyze/steps.rb +4 -0
- data/lib/aidp/cli/jobs_command.rb +2 -1
- data/lib/aidp/cli.rb +812 -3
- data/lib/aidp/concurrency/backoff.rb +148 -0
- data/lib/aidp/concurrency/exec.rb +192 -0
- data/lib/aidp/concurrency/wait.rb +148 -0
- data/lib/aidp/concurrency.rb +71 -0
- data/lib/aidp/config.rb +20 -0
- data/lib/aidp/daemon/runner.rb +9 -8
- data/lib/aidp/debug_mixin.rb +1 -0
- data/lib/aidp/errors.rb +12 -0
- data/lib/aidp/execute/interactive_repl.rb +102 -11
- data/lib/aidp/execute/repl_macros.rb +776 -2
- data/lib/aidp/execute/runner.rb +27 -5
- data/lib/aidp/execute/steps.rb +2 -0
- data/lib/aidp/harness/config_loader.rb +24 -2
- data/lib/aidp/harness/enhanced_runner.rb +16 -2
- data/lib/aidp/harness/error_handler.rb +1 -1
- data/lib/aidp/harness/provider_info.rb +19 -15
- data/lib/aidp/harness/provider_manager.rb +47 -41
- data/lib/aidp/harness/runner.rb +3 -11
- data/lib/aidp/harness/state/persistence.rb +1 -6
- data/lib/aidp/harness/state_manager.rb +115 -7
- data/lib/aidp/harness/status_display.rb +11 -18
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
- data/lib/aidp/harness/user_interface.rb +12 -15
- data/lib/aidp/jobs/background_runner.rb +15 -5
- data/lib/aidp/providers/codex.rb +0 -1
- data/lib/aidp/providers/cursor.rb +0 -1
- data/lib/aidp/providers/github_copilot.rb +0 -1
- data/lib/aidp/providers/opencode.rb +0 -1
- data/lib/aidp/skills/composer.rb +178 -0
- data/lib/aidp/skills/loader.rb +205 -0
- data/lib/aidp/skills/registry.rb +220 -0
- data/lib/aidp/skills/skill.rb +174 -0
- data/lib/aidp/skills.rb +30 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +93 -28
- data/lib/aidp/watch/runner.rb +3 -2
- data/lib/aidp/workstream_executor.rb +244 -0
- data/lib/aidp/workstream_state.rb +212 -0
- data/lib/aidp/worktree.rb +208 -0
- data/lib/aidp.rb +6 -0
- 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
|