ace-retro 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 +7 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-retro.yml +19 -0
- data/.ace-defaults/retro/config.yml +16 -0
- data/CHANGELOG.md +252 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +13 -0
- data/docs/demo/ace-retro-getting-started.gif +0 -0
- data/docs/demo/ace-retro-getting-started.tape.yml +33 -0
- data/docs/demo/fixtures/README.md +3 -0
- data/docs/demo/fixtures/sample.txt +1 -0
- data/docs/getting-started.md +77 -0
- data/docs/handbook.md +60 -0
- data/docs/usage.md +141 -0
- data/exe/ace-retro +22 -0
- data/handbook/skills/as-handbook-selfimprove/SKILL.md +31 -0
- data/handbook/skills/as-retro-create/SKILL.md +26 -0
- data/handbook/skills/as-retro-synthesize/SKILL.md +26 -0
- data/handbook/templates/retro/retro.template.md +194 -0
- data/handbook/workflow-instructions/retro/create.wf.md +141 -0
- data/handbook/workflow-instructions/retro/selfimprove.wf.md +197 -0
- data/handbook/workflow-instructions/retro/synthesize.wf.md +94 -0
- data/lib/ace/retro/atoms/retro_file_pattern.rb +40 -0
- data/lib/ace/retro/atoms/retro_frontmatter_defaults.rb +42 -0
- data/lib/ace/retro/atoms/retro_id_formatter.rb +37 -0
- data/lib/ace/retro/atoms/retro_validation_rules.rb +82 -0
- data/lib/ace/retro/cli/commands/create.rb +87 -0
- data/lib/ace/retro/cli/commands/doctor.rb +204 -0
- data/lib/ace/retro/cli/commands/list.rb +63 -0
- data/lib/ace/retro/cli/commands/show.rb +55 -0
- data/lib/ace/retro/cli/commands/update.rb +117 -0
- data/lib/ace/retro/cli.rb +70 -0
- data/lib/ace/retro/models/retro.rb +40 -0
- data/lib/ace/retro/molecules/retro_config_loader.rb +93 -0
- data/lib/ace/retro/molecules/retro_creator.rb +165 -0
- data/lib/ace/retro/molecules/retro_display_formatter.rb +95 -0
- data/lib/ace/retro/molecules/retro_doctor_fixer.rb +404 -0
- data/lib/ace/retro/molecules/retro_doctor_reporter.rb +257 -0
- data/lib/ace/retro/molecules/retro_frontmatter_validator.rb +120 -0
- data/lib/ace/retro/molecules/retro_loader.rb +119 -0
- data/lib/ace/retro/molecules/retro_mover.rb +80 -0
- data/lib/ace/retro/molecules/retro_resolver.rb +57 -0
- data/lib/ace/retro/molecules/retro_scanner.rb +56 -0
- data/lib/ace/retro/molecules/retro_structure_validator.rb +193 -0
- data/lib/ace/retro/organisms/retro_doctor.rb +199 -0
- data/lib/ace/retro/organisms/retro_manager.rb +210 -0
- data/lib/ace/retro/version.rb +7 -0
- data/lib/ace/retro.rb +41 -0
- metadata +165 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require "ace/llm"
|
|
6
|
+
require_relative "../../organisms/retro_doctor"
|
|
7
|
+
require_relative "../../molecules/retro_doctor_fixer"
|
|
8
|
+
require_relative "../../molecules/retro_doctor_reporter"
|
|
9
|
+
require_relative "../../molecules/retro_config_loader"
|
|
10
|
+
|
|
11
|
+
module Ace
|
|
12
|
+
module Retro
|
|
13
|
+
module CLI
|
|
14
|
+
module Commands
|
|
15
|
+
# ace-support-cli Command class for ace-retro doctor
|
|
16
|
+
#
|
|
17
|
+
# Runs health checks on retros and optionally auto-fixes issues.
|
|
18
|
+
class Doctor < Ace::Support::Cli::Command
|
|
19
|
+
include Ace::Support::Cli::Base
|
|
20
|
+
|
|
21
|
+
desc <<~DESC.strip
|
|
22
|
+
Run health checks on retros
|
|
23
|
+
|
|
24
|
+
Validates frontmatter, file structure, and scope/status consistency
|
|
25
|
+
across all retros in the repository. Supports auto-fixing safe issues.
|
|
26
|
+
|
|
27
|
+
DESC
|
|
28
|
+
|
|
29
|
+
example [
|
|
30
|
+
" # Run all health checks",
|
|
31
|
+
"--auto-fix # Auto-fix safe issues",
|
|
32
|
+
"--auto-fix --dry-run # Preview fixes without applying",
|
|
33
|
+
"--auto-fix-with-agent # Auto-fix then launch agent for remaining",
|
|
34
|
+
"--check frontmatter # Run specific check (frontmatter|structure|scope)",
|
|
35
|
+
"--json # Output as JSON",
|
|
36
|
+
"--verbose # Show all warnings"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
40
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
41
|
+
option :auto_fix, type: :boolean, aliases: %w[-f], desc: "Auto-fix safe issues"
|
|
42
|
+
option :auto_fix_with_agent, type: :boolean, desc: "Auto-fix then launch agent for remaining"
|
|
43
|
+
option :model, type: :string, desc: "Provider:model for agent session"
|
|
44
|
+
option :errors_only, type: :boolean, desc: "Show only errors, not warnings"
|
|
45
|
+
option :no_color, type: :boolean, desc: "Disable colored output"
|
|
46
|
+
option :json, type: :boolean, desc: "Output in JSON format"
|
|
47
|
+
option :dry_run, type: :boolean, aliases: %w[-n], desc: "Preview fixes without applying"
|
|
48
|
+
option :check, type: :string, desc: "Run specific check (frontmatter, structure, scope)"
|
|
49
|
+
|
|
50
|
+
def call(**options)
|
|
51
|
+
execute_doctor(options)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def execute_doctor(options)
|
|
57
|
+
config = Molecules::RetroConfigLoader.load
|
|
58
|
+
root_dir = Molecules::RetroConfigLoader.root_dir(config)
|
|
59
|
+
|
|
60
|
+
unless Dir.exist?(root_dir)
|
|
61
|
+
puts "Error: Retros directory not found: #{root_dir}"
|
|
62
|
+
raise Ace::Support::Cli::Error.new("Retros directory not found")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
format = options[:json] ? :json : :terminal
|
|
66
|
+
fix = options[:auto_fix] || options[:auto_fix_with_agent]
|
|
67
|
+
colors = !options[:no_color]
|
|
68
|
+
colors = false if format == :json
|
|
69
|
+
|
|
70
|
+
doctor_opts = {}
|
|
71
|
+
doctor_opts[:check] = options[:check] if options[:check]
|
|
72
|
+
|
|
73
|
+
if options[:quiet]
|
|
74
|
+
results = run_diagnosis(root_dir, doctor_opts)
|
|
75
|
+
raise Ace::Support::Cli::Error.new("Health check failed") unless results[:valid]
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
results = run_diagnosis(root_dir, doctor_opts)
|
|
80
|
+
|
|
81
|
+
if options[:errors_only] && results[:issues]
|
|
82
|
+
results[:issues] = results[:issues].select { |i| i[:type] == :error }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
output = Molecules::RetroDoctorReporter.format_results(
|
|
86
|
+
results,
|
|
87
|
+
format: format,
|
|
88
|
+
verbose: options[:verbose],
|
|
89
|
+
colors: colors
|
|
90
|
+
)
|
|
91
|
+
puts output
|
|
92
|
+
|
|
93
|
+
if fix && results[:issues]&.any?
|
|
94
|
+
handle_auto_fix(results, root_dir, doctor_opts, options, colors)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if options[:auto_fix_with_agent]
|
|
98
|
+
handle_agent_fix(root_dir, doctor_opts, options, config)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
raise Ace::Support::Cli::Error.new("Health check failed") unless results[:valid]
|
|
102
|
+
rescue Ace::Support::Cli::Error
|
|
103
|
+
raise
|
|
104
|
+
rescue => e
|
|
105
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def run_diagnosis(root_dir, doctor_opts)
|
|
109
|
+
doctor = Organisms::RetroDoctor.new(root_dir, doctor_opts)
|
|
110
|
+
doctor.run_diagnosis
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def handle_auto_fix(results, root_dir, doctor_opts, options, colors)
|
|
114
|
+
doctor = Organisms::RetroDoctor.new(root_dir, doctor_opts)
|
|
115
|
+
fixable_issues = results[:issues].select { |issue| doctor.auto_fixable?(issue) }
|
|
116
|
+
|
|
117
|
+
if fixable_issues.empty?
|
|
118
|
+
puts "\nNo auto-fixable issues found"
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
unless options[:quiet] || options[:dry_run]
|
|
123
|
+
puts "\nFound #{fixable_issues.size} auto-fixable issues"
|
|
124
|
+
print "Apply fixes? (y/N): "
|
|
125
|
+
response = $stdin.gets.chomp.downcase
|
|
126
|
+
return unless response == "y" || response == "yes"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
fixer = Molecules::RetroDoctorFixer.new(dry_run: options[:dry_run], root_dir: root_dir)
|
|
130
|
+
fix_results = fixer.fix_issues(fixable_issues)
|
|
131
|
+
|
|
132
|
+
output = Molecules::RetroDoctorReporter.format_fix_results(
|
|
133
|
+
fix_results,
|
|
134
|
+
colors: colors
|
|
135
|
+
)
|
|
136
|
+
puts output
|
|
137
|
+
|
|
138
|
+
unless options[:dry_run]
|
|
139
|
+
puts "\nRe-running health check after fixes..."
|
|
140
|
+
new_results = run_diagnosis(root_dir, doctor_opts)
|
|
141
|
+
|
|
142
|
+
output = Molecules::RetroDoctorReporter.format_results(
|
|
143
|
+
new_results,
|
|
144
|
+
format: :summary,
|
|
145
|
+
verbose: false,
|
|
146
|
+
colors: colors
|
|
147
|
+
)
|
|
148
|
+
puts output
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def handle_agent_fix(root_dir, doctor_opts, options, config)
|
|
153
|
+
results = run_diagnosis(root_dir, doctor_opts)
|
|
154
|
+
remaining = results[:issues]&.reject { |i| i[:type] == :info }
|
|
155
|
+
|
|
156
|
+
if remaining.nil? || remaining.empty?
|
|
157
|
+
puts "\nNo remaining issues for agent to fix."
|
|
158
|
+
return
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
issue_list = remaining.map { |i|
|
|
162
|
+
prefix = (i[:type] == :error) ? "ERROR" : "WARNING"
|
|
163
|
+
"- [#{prefix}] #{i[:message]}#{" (#{i[:location]})" if i[:location]}"
|
|
164
|
+
}.join("\n")
|
|
165
|
+
|
|
166
|
+
provider_model = options[:model] || config.dig("retro", "doctor_agent_model") || "gemini:flash-latest@yolo"
|
|
167
|
+
|
|
168
|
+
prompt = <<~PROMPT
|
|
169
|
+
The following #{remaining.size} retro issues could NOT be auto-fixed and need manual intervention:
|
|
170
|
+
|
|
171
|
+
#{issue_list}
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
Fix each issue listed above in the .ace-retros/ directory.
|
|
176
|
+
|
|
177
|
+
IMPORTANT RULES:
|
|
178
|
+
- For invalid ID format issues, inspect the folder name and fix the frontmatter ID to match
|
|
179
|
+
- For YAML syntax errors, read the file and fix the YAML
|
|
180
|
+
- For missing opening delimiter, add '---' at the start of the file
|
|
181
|
+
- Do NOT delete content files — prefer fixing in place
|
|
182
|
+
- For folder naming issues, rename the folder to match {id}-{slug} convention
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
Run `ace-retro doctor --verbose` to verify all issues are fixed.
|
|
187
|
+
PROMPT
|
|
188
|
+
|
|
189
|
+
puts "\nLaunching agent to fix #{remaining.size} remaining issues..."
|
|
190
|
+
query_options = {
|
|
191
|
+
system: nil,
|
|
192
|
+
timeout: 600,
|
|
193
|
+
fallback: false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
response = Ace::LLM::QueryInterface.query(provider_model, prompt, **query_options)
|
|
197
|
+
|
|
198
|
+
puts response[:text]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Retro
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# ace-support-cli Command class for ace-retro list
|
|
10
|
+
class List < Ace::Support::Cli::Command
|
|
11
|
+
include Ace::Support::Cli::Base
|
|
12
|
+
|
|
13
|
+
C = Ace::Support::Items::Atoms::AnsiColors
|
|
14
|
+
desc "List retros\n\n" \
|
|
15
|
+
"Lists all retros with optional filtering by status, type, tags, or folder.\n\n" \
|
|
16
|
+
"Status legend:\n" \
|
|
17
|
+
" #{C::YELLOW}○ active#{C::RESET} #{C::GREEN}✓ done#{C::RESET}"
|
|
18
|
+
remove_const(:C)
|
|
19
|
+
|
|
20
|
+
example [
|
|
21
|
+
" # Active retros (root only, default)",
|
|
22
|
+
"--in all # All retros including archived",
|
|
23
|
+
"--in archive # Retros in _archive/",
|
|
24
|
+
"--status active # Filter by status",
|
|
25
|
+
"--type standard # Filter by type",
|
|
26
|
+
"--tags sprint,team # Retros matching any tag",
|
|
27
|
+
"--in archive --type standard # Combined filters"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
option :status, type: :string, aliases: %w[-s], desc: "Filter by status (active, done)"
|
|
31
|
+
option :type, type: :string, aliases: %w[-t], desc: "Filter by type (standard, conversation-analysis, self-review)"
|
|
32
|
+
option :tags, type: :string, aliases: %w[-T], desc: "Filter by tags (comma-separated, any match)"
|
|
33
|
+
option :in, type: :string, aliases: %w[-i], desc: "Filter by folder (next=root only [default], all=everything, archive)"
|
|
34
|
+
option :root, type: :string, aliases: %w[-r], desc: "Override root path"
|
|
35
|
+
|
|
36
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
37
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
38
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
39
|
+
|
|
40
|
+
def call(**options)
|
|
41
|
+
status = options[:status]
|
|
42
|
+
type = options[:type]
|
|
43
|
+
in_folder = options[:in]
|
|
44
|
+
tags_str = options[:tags]
|
|
45
|
+
tags = tags_str ? tags_str.split(",").map(&:strip).reject(&:empty?) : []
|
|
46
|
+
|
|
47
|
+
manager_opts = {}
|
|
48
|
+
manager_opts[:root_dir] = options[:root] if options[:root]
|
|
49
|
+
manager = Ace::Retro::Organisms::RetroManager.new(**manager_opts)
|
|
50
|
+
list_opts = {status: status, type: type, tags: tags}
|
|
51
|
+
list_opts[:in_folder] = in_folder if in_folder
|
|
52
|
+
retros = manager.list(**list_opts)
|
|
53
|
+
|
|
54
|
+
puts Ace::Retro::Molecules::RetroDisplayFormatter.format_list(
|
|
55
|
+
retros, total_count: manager.last_list_total,
|
|
56
|
+
global_folder_stats: manager.last_folder_counts
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Retro
|
|
7
|
+
module CLI
|
|
8
|
+
module Commands
|
|
9
|
+
# ace-support-cli Command class for ace-retro show
|
|
10
|
+
class Show < Ace::Support::Cli::Command
|
|
11
|
+
include Ace::Support::Cli::Base
|
|
12
|
+
|
|
13
|
+
desc <<~DESC.strip
|
|
14
|
+
Show retro details
|
|
15
|
+
|
|
16
|
+
Displays a retro by reference (full 6-char ID or last 3-char shortcut).
|
|
17
|
+
|
|
18
|
+
DESC
|
|
19
|
+
|
|
20
|
+
example [
|
|
21
|
+
"q7w # Formatted display (default)",
|
|
22
|
+
"8ppq7w --path # Print file path only",
|
|
23
|
+
"q7w --content # Print raw markdown content"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
argument :ref, required: true, desc: "Retro reference (6-char ID or 3-char shortcut)"
|
|
27
|
+
|
|
28
|
+
option :path, type: :boolean, desc: "Print file path only"
|
|
29
|
+
option :content, type: :boolean, desc: "Print raw markdown content"
|
|
30
|
+
|
|
31
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
32
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
33
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
34
|
+
|
|
35
|
+
def call(ref:, **options)
|
|
36
|
+
manager = Ace::Retro::Organisms::RetroManager.new
|
|
37
|
+
retro = manager.show(ref)
|
|
38
|
+
|
|
39
|
+
unless retro
|
|
40
|
+
raise Ace::Support::Cli::Error.new("Retro '#{ref}' not found")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if options[:path]
|
|
44
|
+
puts retro.file_path
|
|
45
|
+
elsif options[:content]
|
|
46
|
+
puts File.read(retro.file_path)
|
|
47
|
+
else
|
|
48
|
+
puts Ace::Retro::Molecules::RetroDisplayFormatter.format(retro, show_content: true)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/support/items"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Retro
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
# ace-support-cli Command class for ace-retro update
|
|
11
|
+
class Update < Ace::Support::Cli::Command
|
|
12
|
+
include Ace::Support::Cli::Base
|
|
13
|
+
|
|
14
|
+
desc <<~DESC.strip
|
|
15
|
+
Update retro metadata and/or move to a folder
|
|
16
|
+
|
|
17
|
+
Updates frontmatter fields using set, add, or remove operations.
|
|
18
|
+
Use --set for scalar fields, --add/--remove for array fields like tags.
|
|
19
|
+
Use --move-to to relocate to a special folder or back to root.
|
|
20
|
+
DESC
|
|
21
|
+
|
|
22
|
+
example [
|
|
23
|
+
"q7w --set status=done",
|
|
24
|
+
'q7w --set status=done --set title="Refined title"',
|
|
25
|
+
"q7w --add tags=reviewed --remove tags=in-progress",
|
|
26
|
+
"q7w --set status=done --add tags=shipped",
|
|
27
|
+
"q7w --set status=done --move-to archive",
|
|
28
|
+
"q7w --move-to next"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
argument :ref, required: true, desc: "Retro reference (6-char ID or 3-char shortcut)"
|
|
32
|
+
|
|
33
|
+
option :set, type: :string, repeat: true, desc: "Set field: key=value (can repeat)"
|
|
34
|
+
option :add, type: :string, repeat: true, desc: "Add to array field: key=value (can repeat)"
|
|
35
|
+
option :remove, type: :string, repeat: true, desc: "Remove from array field: key=value (can repeat)"
|
|
36
|
+
option :move_to, type: :string, aliases: %w[-m], desc: "Move to folder (archive, next)"
|
|
37
|
+
|
|
38
|
+
option :git_commit, type: :boolean, aliases: %w[--gc], desc: "Auto-commit changes"
|
|
39
|
+
|
|
40
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
41
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
42
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
43
|
+
|
|
44
|
+
def call(ref:, **options)
|
|
45
|
+
set_args = Array(options[:set])
|
|
46
|
+
add_args = Array(options[:add])
|
|
47
|
+
remove_args = Array(options[:remove])
|
|
48
|
+
move_to = options[:move_to]
|
|
49
|
+
|
|
50
|
+
if set_args.empty? && add_args.empty? && remove_args.empty? && move_to.nil?
|
|
51
|
+
warn "Error: at least one of --set, --add, --remove, or --move-to is required"
|
|
52
|
+
warn ""
|
|
53
|
+
warn "Usage: ace-retro update REF [--set K=V]... [--add K=V]... [--remove K=V]... [--move-to FOLDER]"
|
|
54
|
+
raise Ace::Support::Cli::Error.new("No update operations specified")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
set_hash = parse_kv_pairs(set_args)
|
|
58
|
+
add_hash = parse_kv_pairs(add_args)
|
|
59
|
+
remove_hash = parse_kv_pairs(remove_args)
|
|
60
|
+
|
|
61
|
+
manager = Ace::Retro::Organisms::RetroManager.new
|
|
62
|
+
retro = manager.update(ref, set: set_hash, add: add_hash, remove: remove_hash, move_to: move_to)
|
|
63
|
+
|
|
64
|
+
unless retro
|
|
65
|
+
raise Ace::Support::Cli::Error.new("Retro '#{ref}' not found")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if move_to
|
|
69
|
+
folder_info = retro.special_folder || "root"
|
|
70
|
+
puts "Retro updated: #{retro.id} #{retro.title} → #{folder_info}"
|
|
71
|
+
else
|
|
72
|
+
puts "Retro updated: #{retro.id} #{retro.title}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if options[:git_commit]
|
|
76
|
+
commit_paths = move_to ? [manager.root_dir] : [retro.path]
|
|
77
|
+
intention = if move_to
|
|
78
|
+
"update retro #{retro.id} and move to #{retro.special_folder || "root"}"
|
|
79
|
+
else
|
|
80
|
+
"update retro #{retro.id}"
|
|
81
|
+
end
|
|
82
|
+
Ace::Support::Items::Molecules::GitCommitter.commit(
|
|
83
|
+
paths: commit_paths,
|
|
84
|
+
intention: intention
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Parse ["key=value", "key=value2"] into {"key" => typed_value, ...}
|
|
92
|
+
# Delegates to FieldArgumentParser for type inference (arrays, booleans, numerics).
|
|
93
|
+
def parse_kv_pairs(args)
|
|
94
|
+
result = {}
|
|
95
|
+
args.each do |arg|
|
|
96
|
+
unless arg.include?("=")
|
|
97
|
+
raise Ace::Support::Cli::Error.new("Invalid format '#{arg}': expected key=value")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
parsed = Ace::Support::Items::Atoms::FieldArgumentParser.parse([arg])
|
|
101
|
+
parsed.each do |key, value|
|
|
102
|
+
result[key] = if result.key?(key)
|
|
103
|
+
Array(result[key]) + Array(value)
|
|
104
|
+
else
|
|
105
|
+
value
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue Ace::Support::Items::Atoms::FieldArgumentParser::ParseError => e
|
|
109
|
+
raise Ace::Support::Cli::Error.new("Invalid argument '#{arg}': #{e.message}")
|
|
110
|
+
end
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "../retro/version"
|
|
6
|
+
require_relative "cli/commands/create"
|
|
7
|
+
require_relative "cli/commands/show"
|
|
8
|
+
require_relative "cli/commands/list"
|
|
9
|
+
require_relative "cli/commands/update"
|
|
10
|
+
require_relative "cli/commands/doctor"
|
|
11
|
+
|
|
12
|
+
module Ace
|
|
13
|
+
module Retro
|
|
14
|
+
# Flat CLI registry for ace-retro (retrospective management).
|
|
15
|
+
#
|
|
16
|
+
# Provides the flat `ace-retro <command>` invocation pattern.
|
|
17
|
+
module RetroCLI
|
|
18
|
+
extend Ace::Support::Cli::RegistryDsl
|
|
19
|
+
|
|
20
|
+
PROGRAM_NAME = "ace-retro"
|
|
21
|
+
|
|
22
|
+
REGISTERED_COMMANDS = [
|
|
23
|
+
["create", "Create a new retro"],
|
|
24
|
+
["show", "Show retro details"],
|
|
25
|
+
["list", "List retros"],
|
|
26
|
+
["update", "Update retro metadata (fields and move)"],
|
|
27
|
+
["doctor", "Run health checks on retros"]
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
HELP_EXAMPLES = [
|
|
31
|
+
'ace-retro create "Sprint Review" --type standard --tags sprint,team',
|
|
32
|
+
"ace-retro show q7w",
|
|
33
|
+
"ace-retro list --in archive --status active",
|
|
34
|
+
"ace-retro update q7w --set status=done --move-to archive",
|
|
35
|
+
"ace-retro update q7w --set status=done --add tags=reviewed",
|
|
36
|
+
"ace-retro update q7w --move-to next",
|
|
37
|
+
"ace-retro doctor --verbose"
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
register "create", CLI::Commands::Create
|
|
41
|
+
register "show", CLI::Commands::Show
|
|
42
|
+
register "list", CLI::Commands::List
|
|
43
|
+
register "update", CLI::Commands::Update
|
|
44
|
+
register "doctor", CLI::Commands::Doctor
|
|
45
|
+
|
|
46
|
+
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
47
|
+
gem_name: "ace-retro",
|
|
48
|
+
version: Ace::Retro::VERSION
|
|
49
|
+
)
|
|
50
|
+
register "version", version_cmd
|
|
51
|
+
register "--version", version_cmd
|
|
52
|
+
|
|
53
|
+
help_cmd = Ace::Support::Cli::HelpCommand.build(
|
|
54
|
+
program_name: PROGRAM_NAME,
|
|
55
|
+
version: Ace::Retro::VERSION,
|
|
56
|
+
commands: REGISTERED_COMMANDS,
|
|
57
|
+
examples: HELP_EXAMPLES
|
|
58
|
+
)
|
|
59
|
+
register "help", help_cmd
|
|
60
|
+
register "--help", help_cmd
|
|
61
|
+
register "-h", help_cmd
|
|
62
|
+
|
|
63
|
+
# Entry point for CLI invocation
|
|
64
|
+
# @param args [Array<String>] Command-line arguments
|
|
65
|
+
def self.start(args)
|
|
66
|
+
Ace::Support::Cli::Runner.new(self).call(args: args)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Retro
|
|
5
|
+
module Models
|
|
6
|
+
# Value object representing a retrospective
|
|
7
|
+
# Holds all metadata and content for a single retro
|
|
8
|
+
Retro = Struct.new(
|
|
9
|
+
:id, # Raw 6-char b36ts ID (e.g., "8ppq7w")
|
|
10
|
+
:status, # Status string: "active", "done"
|
|
11
|
+
:title, # Human-readable title
|
|
12
|
+
:type, # Retro type: "standard", "conversation-analysis", "self-review"
|
|
13
|
+
:tags, # Array of tag strings
|
|
14
|
+
:content, # Body content (markdown, excluding frontmatter)
|
|
15
|
+
:path, # Directory path for the retro folder
|
|
16
|
+
:file_path, # Full path to the .retro.md file
|
|
17
|
+
:special_folder, # Special folder if any (e.g., "_archive", nil)
|
|
18
|
+
:created_at, # Time object for creation time
|
|
19
|
+
:folder_contents, # Array of additional filenames in the retro folder
|
|
20
|
+
:metadata, # Additional frontmatter fields as Hash
|
|
21
|
+
keyword_init: true
|
|
22
|
+
) do
|
|
23
|
+
# Display-friendly representation
|
|
24
|
+
def to_s
|
|
25
|
+
"Retro(#{id}: #{title})"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Short reference (last 3 chars of ID)
|
|
29
|
+
def shortcut
|
|
30
|
+
id[-3..] if id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if retro is in a special folder
|
|
34
|
+
def special?
|
|
35
|
+
!special_folder.nil?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "ace/support/fs"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Retro
|
|
8
|
+
module Molecules
|
|
9
|
+
# Loads and merges configuration for ace-retro from the cascade:
|
|
10
|
+
# .ace-defaults/retro/config.yml (gem) -> ~/.ace/retro/config.yml (user) -> .ace/retro/config.yml (project)
|
|
11
|
+
class RetroConfigLoader
|
|
12
|
+
DEFAULT_ROOT_DIR = ".ace-retros"
|
|
13
|
+
|
|
14
|
+
# Load configuration with cascade merge
|
|
15
|
+
# @param gem_root [String] Path to the ace-retro gem root
|
|
16
|
+
# @return [Hash] Merged configuration
|
|
17
|
+
def self.load(gem_root: nil)
|
|
18
|
+
gem_root ||= File.expand_path("../../../..", __dir__)
|
|
19
|
+
# lib/ace/retro/molecules/ → 4 levels up to gem root
|
|
20
|
+
new(gem_root: gem_root).load
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(gem_root:)
|
|
24
|
+
@gem_root = gem_root
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Load and merge configuration
|
|
28
|
+
# @return [Hash] Merged configuration
|
|
29
|
+
def load
|
|
30
|
+
config = load_defaults
|
|
31
|
+
config = deep_merge(config, load_user_config)
|
|
32
|
+
deep_merge(config, load_project_config)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get the root directory for retros
|
|
36
|
+
# @param config [Hash] Configuration hash
|
|
37
|
+
# @return [String] Absolute path to retros root directory
|
|
38
|
+
def self.root_dir(config = nil)
|
|
39
|
+
config ||= load
|
|
40
|
+
dir = config.dig("retro", "root_dir") || DEFAULT_ROOT_DIR
|
|
41
|
+
|
|
42
|
+
if dir.start_with?("/")
|
|
43
|
+
dir
|
|
44
|
+
else
|
|
45
|
+
File.join(Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current, dir)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def load_defaults
|
|
52
|
+
path = File.join(@gem_root, ".ace-defaults", "retro", "config.yml")
|
|
53
|
+
load_yaml(path) || {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_user_config
|
|
57
|
+
path = File.join(Dir.home, ".ace", "retro", "config.yml")
|
|
58
|
+
load_yaml(path) || {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load_project_config
|
|
62
|
+
path = File.join(Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current, ".ace", "retro", "config.yml")
|
|
63
|
+
load_yaml(path) || {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_yaml(path)
|
|
67
|
+
return nil unless File.exist?(path)
|
|
68
|
+
|
|
69
|
+
YAML.safe_load_file(path, permitted_classes: [Date, Time, Symbol])
|
|
70
|
+
rescue Errno::ENOENT
|
|
71
|
+
nil
|
|
72
|
+
rescue Psych::SyntaxError => e
|
|
73
|
+
warn "Warning: ace-retro config parse error in #{path}: #{e.message}"
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def deep_merge(base, override)
|
|
78
|
+
return base unless override.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
result = base.dup
|
|
81
|
+
override.each do |key, value|
|
|
82
|
+
result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
|
|
83
|
+
deep_merge(result[key], value)
|
|
84
|
+
else
|
|
85
|
+
value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|