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.
- checksums.yaml +4 -4
- data/README.md +47 -0
- data/lib/aidp/analyze/error_handler.rb +46 -28
- data/lib/aidp/analyze/progress.rb +1 -1
- 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 +1086 -4
- 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 +21 -1
- 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/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint.rb +1 -1
- data/lib/aidp/execute/future_work_backlog.rb +1 -1
- data/lib/aidp/execute/interactive_repl.rb +102 -11
- data/lib/aidp/execute/progress.rb +1 -1
- data/lib/aidp/execute/repl_macros.rb +845 -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/config_validator.rb +1 -1
- 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 +16 -6
- 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 +222 -0
- data/lib/aidp/skills/router.rb +178 -0
- data/lib/aidp/skills/skill.rb +174 -0
- data/lib/aidp/skills/wizard/builder.rb +141 -0
- data/lib/aidp/skills/wizard/controller.rb +145 -0
- data/lib/aidp/skills/wizard/differ.rb +232 -0
- data/lib/aidp/skills/wizard/prompter.rb +317 -0
- data/lib/aidp/skills/wizard/template_library.rb +164 -0
- data/lib/aidp/skills/wizard/writer.rb +105 -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
- data/templates/skills/README.md +334 -0
- data/templates/skills/architecture_analyst/SKILL.md +173 -0
- data/templates/skills/product_strategist/SKILL.md +141 -0
- data/templates/skills/repository_analyst/SKILL.md +117 -0
- data/templates/skills/test_analyzer/SKILL.md +213 -0
- 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
|