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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/nav/protocols/wfi-sources/ace-retro.yml +19 -0
  3. data/.ace-defaults/retro/config.yml +16 -0
  4. data/CHANGELOG.md +252 -0
  5. data/LICENSE +21 -0
  6. data/README.md +40 -0
  7. data/Rakefile +13 -0
  8. data/docs/demo/ace-retro-getting-started.gif +0 -0
  9. data/docs/demo/ace-retro-getting-started.tape.yml +33 -0
  10. data/docs/demo/fixtures/README.md +3 -0
  11. data/docs/demo/fixtures/sample.txt +1 -0
  12. data/docs/getting-started.md +77 -0
  13. data/docs/handbook.md +60 -0
  14. data/docs/usage.md +141 -0
  15. data/exe/ace-retro +22 -0
  16. data/handbook/skills/as-handbook-selfimprove/SKILL.md +31 -0
  17. data/handbook/skills/as-retro-create/SKILL.md +26 -0
  18. data/handbook/skills/as-retro-synthesize/SKILL.md +26 -0
  19. data/handbook/templates/retro/retro.template.md +194 -0
  20. data/handbook/workflow-instructions/retro/create.wf.md +141 -0
  21. data/handbook/workflow-instructions/retro/selfimprove.wf.md +197 -0
  22. data/handbook/workflow-instructions/retro/synthesize.wf.md +94 -0
  23. data/lib/ace/retro/atoms/retro_file_pattern.rb +40 -0
  24. data/lib/ace/retro/atoms/retro_frontmatter_defaults.rb +42 -0
  25. data/lib/ace/retro/atoms/retro_id_formatter.rb +37 -0
  26. data/lib/ace/retro/atoms/retro_validation_rules.rb +82 -0
  27. data/lib/ace/retro/cli/commands/create.rb +87 -0
  28. data/lib/ace/retro/cli/commands/doctor.rb +204 -0
  29. data/lib/ace/retro/cli/commands/list.rb +63 -0
  30. data/lib/ace/retro/cli/commands/show.rb +55 -0
  31. data/lib/ace/retro/cli/commands/update.rb +117 -0
  32. data/lib/ace/retro/cli.rb +70 -0
  33. data/lib/ace/retro/models/retro.rb +40 -0
  34. data/lib/ace/retro/molecules/retro_config_loader.rb +93 -0
  35. data/lib/ace/retro/molecules/retro_creator.rb +165 -0
  36. data/lib/ace/retro/molecules/retro_display_formatter.rb +95 -0
  37. data/lib/ace/retro/molecules/retro_doctor_fixer.rb +404 -0
  38. data/lib/ace/retro/molecules/retro_doctor_reporter.rb +257 -0
  39. data/lib/ace/retro/molecules/retro_frontmatter_validator.rb +120 -0
  40. data/lib/ace/retro/molecules/retro_loader.rb +119 -0
  41. data/lib/ace/retro/molecules/retro_mover.rb +80 -0
  42. data/lib/ace/retro/molecules/retro_resolver.rb +57 -0
  43. data/lib/ace/retro/molecules/retro_scanner.rb +56 -0
  44. data/lib/ace/retro/molecules/retro_structure_validator.rb +193 -0
  45. data/lib/ace/retro/organisms/retro_doctor.rb +199 -0
  46. data/lib/ace/retro/organisms/retro_manager.rb +210 -0
  47. data/lib/ace/retro/version.rb +7 -0
  48. data/lib/ace/retro.rb +41 -0
  49. metadata +165 -0
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../atoms/retro_file_pattern"
4
+ require_relative "../atoms/retro_id_formatter"
5
+
6
+ module Ace
7
+ module Retro
8
+ module Molecules
9
+ # Validates the directory structure of a retros root directory.
10
+ # Checks folder naming, file naming, and structural conventions.
11
+ class RetroStructureValidator
12
+ # @param root_dir [String] Root directory for retros (e.g., ".ace-retros")
13
+ def initialize(root_dir)
14
+ @root_dir = root_dir
15
+ end
16
+
17
+ # Validate the entire retros directory structure
18
+ # @return [Array<Hash>] List of issues found
19
+ def validate(root_dir = @root_dir)
20
+ issues = []
21
+
22
+ unless Dir.exist?(root_dir)
23
+ issues << {type: :error, message: "Retros root directory does not exist", location: root_dir}
24
+ return issues
25
+ end
26
+
27
+ check_folder_naming(root_dir, issues)
28
+ check_retro_files(root_dir, issues)
29
+ check_archive_partitions(root_dir, issues)
30
+ check_stale_backups(root_dir, issues)
31
+ check_empty_directories(root_dir, issues)
32
+
33
+ issues
34
+ end
35
+
36
+ private
37
+
38
+ # Check that retro folders follow {6-char-id}-{slug} naming
39
+ def check_folder_naming(root_dir, issues)
40
+ retro_dirs(root_dir).each do |dir|
41
+ folder_name = File.basename(dir)
42
+ next if folder_name.start_with?("_")
43
+
44
+ unless folder_name.match?(/^[0-9a-z]{6}-.+$/)
45
+ issues << {
46
+ type: :error,
47
+ message: "Folder name does not match '{id}-{slug}' convention: '#{folder_name}'",
48
+ location: dir
49
+ }
50
+ end
51
+ end
52
+ end
53
+
54
+ # Check that each retro folder has exactly one .retro.md file
55
+ def check_retro_files(root_dir, issues)
56
+ retro_dirs(root_dir).each do |dir|
57
+ folder_name = File.basename(dir)
58
+ next if folder_name.start_with?("_")
59
+
60
+ retro_files = Dir.glob(File.join(dir, Atoms::RetroFilePattern::FILE_GLOB))
61
+
62
+ if retro_files.empty?
63
+ issues << {
64
+ type: :warning,
65
+ message: "No .retro.md file in retro folder",
66
+ location: dir
67
+ }
68
+ elsif retro_files.size > 1
69
+ issues << {
70
+ type: :warning,
71
+ message: "Multiple .retro.md files in folder (#{retro_files.size} found)",
72
+ location: dir
73
+ }
74
+ end
75
+ end
76
+ end
77
+
78
+ # Check for stale backup/tmp files
79
+ def check_stale_backups(root_dir, issues)
80
+ backup_patterns = [
81
+ File.join(root_dir, "**", "*.backup.*"),
82
+ File.join(root_dir, "**", "*.tmp"),
83
+ File.join(root_dir, "**", "*~")
84
+ ]
85
+
86
+ backup_patterns.each do |pattern|
87
+ Dir.glob(pattern).each do |file|
88
+ next if file.include?("/.git/")
89
+
90
+ issues << {
91
+ type: :warning,
92
+ message: "Stale backup file (safe to delete)",
93
+ location: file
94
+ }
95
+ end
96
+ end
97
+ end
98
+
99
+ # Check for empty directories
100
+ def check_empty_directories(root_dir, issues)
101
+ Dir.glob(File.join(root_dir, "**", "*")).each do |path|
102
+ next unless File.directory?(path)
103
+ next if path.include?("/.git/")
104
+
105
+ files = Dir.glob(File.join(path, "**", "*")).select { |f| File.file?(f) }
106
+ if files.empty?
107
+ issues << {
108
+ type: :warning,
109
+ message: "Empty directory (safe to delete)",
110
+ location: path
111
+ }
112
+ end
113
+ end
114
+ end
115
+
116
+ # Check that _archive/ partition directories use valid b36ts names
117
+ def check_archive_partitions(root_dir, issues)
118
+ archive_dir = File.join(root_dir, "_archive")
119
+ return unless Dir.exist?(archive_dir)
120
+
121
+ Dir.glob(File.join(archive_dir, "*")).each do |path|
122
+ next unless File.directory?(path)
123
+ next unless category_folder?(path)
124
+
125
+ partition_name = File.basename(path)
126
+ unless valid_b36ts_partition?(partition_name)
127
+ issues << {
128
+ type: :error,
129
+ message: "Invalid archive partition '#{partition_name}' (expected b36ts like '8o')",
130
+ location: path
131
+ }
132
+ end
133
+ end
134
+ end
135
+
136
+ # Valid b36ts month partitions are 1-3 char lowercase base36 strings
137
+ def valid_b36ts_partition?(name)
138
+ name.match?(/\A[0-9a-z]{1,3}\z/)
139
+ end
140
+
141
+ # Find all immediate subdirectories that look like retro folders
142
+ # (excludes special folders which are containers, includes their children)
143
+ # Also excludes category folders (folders containing only subdirectories)
144
+ # For _archive/, recurses into partition dirs (category folders) to find retros inside them
145
+ def retro_dirs(root_dir)
146
+ dirs = []
147
+
148
+ Dir.glob(File.join(root_dir, "*")).each do |path|
149
+ next unless File.directory?(path)
150
+
151
+ folder_name = File.basename(path)
152
+ if folder_name == "_archive"
153
+ Dir.glob(File.join(path, "*")).each do |subpath|
154
+ next unless File.directory?(subpath)
155
+
156
+ if category_folder?(subpath)
157
+ # Partition dir — recurse into it to find retro folders
158
+ Dir.glob(File.join(subpath, "*")).each do |retro_path|
159
+ dirs << retro_path if File.directory?(retro_path)
160
+ end
161
+ else
162
+ dirs << subpath
163
+ end
164
+ end
165
+ elsif folder_name.start_with?("_")
166
+ Dir.glob(File.join(path, "*")).each do |subpath|
167
+ dirs << subpath if File.directory?(subpath) && !category_folder?(subpath)
168
+ end
169
+ else
170
+ dirs << path unless category_folder?(path)
171
+ end
172
+ end
173
+
174
+ dirs
175
+ end
176
+
177
+ # Check if a folder is a category folder (only contains subdirectories, no files)
178
+ # These are organizational folders and not individual retros
179
+ def category_folder?(dir_path)
180
+ return false unless Dir.exist?(dir_path)
181
+
182
+ # Check if folder has any files
183
+ files = Dir.glob(File.join(dir_path, "*")).select { |f| File.file?(f) }
184
+ return false if files.any?
185
+
186
+ # Check if folder has any subdirectories
187
+ subdirs = Dir.glob(File.join(dir_path, "*")).select { |f| File.directory?(f) }
188
+ subdirs.any?
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../molecules/retro_scanner"
4
+ require_relative "../molecules/retro_loader"
5
+ require_relative "../molecules/retro_frontmatter_validator"
6
+ require_relative "../molecules/retro_structure_validator"
7
+ require_relative "../atoms/retro_validation_rules"
8
+
9
+ module Ace
10
+ module Retro
11
+ module Organisms
12
+ # Orchestrates comprehensive health checks for the retros system.
13
+ # Runs structure validation, frontmatter validation, and scope/status
14
+ # consistency checks across all retros in a root directory.
15
+ class RetroDoctor
16
+ attr_reader :root_path, :options
17
+
18
+ # @param root_path [String] Path to retros root directory
19
+ # @param options [Hash] Diagnosis options (:check, :verbose, etc.)
20
+ def initialize(root_path, options = {})
21
+ @root_path = root_path
22
+ @options = options
23
+ @issues = []
24
+ @stats = {
25
+ retros_scanned: 0,
26
+ folders_checked: 0,
27
+ errors: 0,
28
+ warnings: 0,
29
+ info: 0
30
+ }
31
+ end
32
+
33
+ # Run comprehensive health check
34
+ # @return [Hash] Diagnosis results
35
+ def run_diagnosis
36
+ unless @root_path && Dir.exist?(@root_path)
37
+ return {
38
+ valid: false,
39
+ health_score: 0,
40
+ issues: [{type: :error, message: "Retros root directory not found: #{@root_path}"}],
41
+ stats: @stats,
42
+ duration: 0,
43
+ root_path: @root_path
44
+ }
45
+ end
46
+
47
+ @start_time = Time.now
48
+
49
+ if options[:check]
50
+ run_specific_check(options[:check])
51
+ else
52
+ run_full_check
53
+ end
54
+
55
+ health_score = calculate_health_score
56
+
57
+ {
58
+ valid: @stats[:errors] == 0,
59
+ health_score: health_score,
60
+ issues: @issues,
61
+ stats: @stats,
62
+ duration: Time.now - @start_time,
63
+ root_path: @root_path
64
+ }
65
+ end
66
+
67
+ # Check if an issue can be auto-fixed
68
+ # @param issue [Hash] Issue to check
69
+ # @return [Boolean]
70
+ def auto_fixable?(issue)
71
+ return false unless issue[:type] == :error || issue[:type] == :warning
72
+
73
+ Molecules::RetroDoctorFixer::FIXABLE_PATTERNS.any? { |pattern| issue[:message].match?(pattern) }
74
+ end
75
+
76
+ private
77
+
78
+ def run_full_check
79
+ run_structure_check
80
+ run_frontmatter_check
81
+ run_scope_check
82
+ end
83
+
84
+ def run_specific_check(check_type)
85
+ case check_type.to_s
86
+ when "structure"
87
+ run_structure_check
88
+ when "frontmatter"
89
+ run_frontmatter_check
90
+ when "scope"
91
+ run_scope_check
92
+ else
93
+ add_issue(:error, "Unknown check type: #{check_type}")
94
+ end
95
+ end
96
+
97
+ def run_structure_check
98
+ validator = Molecules::RetroStructureValidator.new(@root_path)
99
+ issues = validator.validate
100
+
101
+ issues.each do |issue|
102
+ add_issue(issue[:type], issue[:message], issue[:location])
103
+ end
104
+
105
+ @stats[:folders_checked] = count_retro_folders
106
+ end
107
+
108
+ def run_frontmatter_check
109
+ scanner = Molecules::RetroScanner.new(@root_path)
110
+ return unless scanner.root_exists?
111
+
112
+ scan_results = scanner.scan
113
+ @stats[:retros_scanned] = scan_results.size
114
+
115
+ scan_results.each do |scan_result|
116
+ spec_file = scan_result.file_path
117
+ next unless spec_file && File.exist?(spec_file)
118
+
119
+ issues = Molecules::RetroFrontmatterValidator.validate(
120
+ spec_file,
121
+ special_folder: scan_result.special_folder
122
+ )
123
+
124
+ # Filter out scope issues (handled separately in run_scope_check)
125
+ issues.reject! { |i| scope_issue?(i[:message]) }
126
+
127
+ issues.each do |issue|
128
+ add_issue(issue[:type], issue[:message], issue[:location])
129
+ end
130
+ end
131
+ end
132
+
133
+ def run_scope_check
134
+ scanner = Molecules::RetroScanner.new(@root_path)
135
+ return unless scanner.root_exists?
136
+
137
+ scan_results = scanner.scan
138
+
139
+ scan_results.each do |scan_result|
140
+ spec_file = scan_result.file_path
141
+ next unless spec_file && File.exist?(spec_file)
142
+
143
+ content = File.read(spec_file)
144
+ frontmatter, _body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
145
+ next unless frontmatter.is_a?(Hash)
146
+
147
+ status = frontmatter["status"]
148
+ special_folder = scan_result.special_folder
149
+
150
+ scope_issues = Atoms::RetroValidationRules.scope_consistent?(status, special_folder)
151
+ scope_issues.each do |issue|
152
+ add_issue(issue[:type], issue[:message], spec_file)
153
+ end
154
+ end
155
+ end
156
+
157
+ def scope_issue?(message)
158
+ message.include?("not in _archive") ||
159
+ message.include?("in _archive/ but status")
160
+ end
161
+
162
+ def count_retro_folders
163
+ return 0 unless Dir.exist?(@root_path)
164
+
165
+ count = 0
166
+ Dir.glob(File.join(@root_path, "*")).each do |path|
167
+ next unless File.directory?(path)
168
+
169
+ count += if File.basename(path).start_with?("_")
170
+ Dir.glob(File.join(path, "*")).count { |p| File.directory?(p) }
171
+ else
172
+ 1
173
+ end
174
+ end
175
+ count
176
+ end
177
+
178
+ def add_issue(type, message, location = nil)
179
+ issue = {type: type, message: message}
180
+ issue[:location] = location if location
181
+ @issues << issue
182
+
183
+ case type
184
+ when :error then @stats[:errors] += 1
185
+ when :warning then @stats[:warnings] += 1
186
+ when :info then @stats[:info] += 1
187
+ end
188
+ end
189
+
190
+ def calculate_health_score
191
+ score = 100
192
+ score -= @stats[:errors] * 10
193
+ score -= @stats[:warnings] * 2
194
+ [[score, 0].max, 100].min
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "../atoms/retro_frontmatter_defaults"
5
+ require_relative "../molecules/retro_config_loader"
6
+ require_relative "../molecules/retro_scanner"
7
+ require_relative "../molecules/retro_resolver"
8
+ require_relative "../molecules/retro_loader"
9
+ require_relative "../molecules/retro_creator"
10
+ require_relative "../molecules/retro_mover"
11
+
12
+ module Ace
13
+ module Retro
14
+ module Organisms
15
+ # Orchestrates all retro CRUD operations.
16
+ # Entry point for retro management with config-driven root directory.
17
+ class RetroManager
18
+ attr_reader :last_list_total, :last_folder_counts
19
+
20
+ # @param root_dir [String, nil] Override root directory for retros
21
+ # @param config [Hash, nil] Override configuration
22
+ def initialize(root_dir: nil, config: nil)
23
+ @config = config || load_config
24
+ @root_dir = root_dir || resolve_root_dir
25
+ end
26
+
27
+ # Create a new retro
28
+ # @param title [String] Retro title
29
+ # @param type [String, nil] Retro type (standard, conversation-analysis, self-review)
30
+ # @param tags [Array<String>] Tags
31
+ # @param move_to [String, nil] Target folder
32
+ # @return [Retro] Created retro
33
+ def create(title, type: nil, tags: [], move_to: nil)
34
+ ensure_root_dir
35
+ creator = Molecules::RetroCreator.new(root_dir: @root_dir, config: @config)
36
+ creator.create(title, type: type, tags: tags, move_to: move_to)
37
+ end
38
+
39
+ # Show (load) a single retro by reference
40
+ # @param ref [String] Full ID (6 chars) or suffix shortcut (3 chars)
41
+ # @return [Retro, nil] Loaded retro or nil if not found
42
+ def show(ref)
43
+ resolver = Molecules::RetroResolver.new(@root_dir)
44
+ scan_result = resolver.resolve(ref)
45
+ return nil unless scan_result
46
+
47
+ loader = Molecules::RetroLoader.new
48
+ loader.load(scan_result.dir_path,
49
+ id: scan_result.id,
50
+ special_folder: scan_result.special_folder)
51
+ end
52
+
53
+ # List retros with optional filtering
54
+ # @param status [String, nil] Filter by status
55
+ # @param type [String, nil] Filter by type
56
+ # @param in_folder [String, nil] Filter by special folder (default: "next" = root items only)
57
+ # @param tags [Array<String>] Filter by tags (any match)
58
+ # @return [Array<Retro>] List of retros
59
+ def list(status: nil, type: nil, in_folder: "next", tags: [])
60
+ scanner = Molecules::RetroScanner.new(@root_dir)
61
+ scan_results = scanner.scan_in_folder(in_folder)
62
+ @last_list_total = scanner.last_scan_total
63
+ @last_folder_counts = scanner.last_folder_counts
64
+
65
+ loader = Molecules::RetroLoader.new
66
+ retros = scan_results.filter_map do |sr|
67
+ loader.load(sr.dir_path, id: sr.id, special_folder: sr.special_folder)
68
+ end
69
+
70
+ retros = retros.select { |r| r.status == status } if status
71
+ retros = retros.select { |r| r.type == type } if type
72
+ retros = filter_by_tags(retros, tags) if tags.any?
73
+
74
+ retros
75
+ end
76
+
77
+ # Update a retro's fields and optionally move to a folder.
78
+ # @param ref [String] Retro reference
79
+ # @param set [Hash] Fields to set (key => value)
80
+ # @param add [Hash] Fields to add to (for arrays like tags)
81
+ # @param remove [Hash] Fields to remove from (for arrays)
82
+ # @param move_to [String, nil] Target folder to move to (archive, maybe, next/root//)
83
+ # @return [Retro, nil] Updated retro or nil if not found
84
+ def update(ref, set: {}, add: {}, remove: {}, move_to: nil)
85
+ scan_result = resolve_scan_result(ref)
86
+ return nil unless scan_result
87
+
88
+ loader = Molecules::RetroLoader.new
89
+ retro = loader.load(scan_result.dir_path,
90
+ id: scan_result.id,
91
+ special_folder: scan_result.special_folder)
92
+ return nil unless retro
93
+
94
+ # Apply field updates if any
95
+ has_field_updates = [set, add, remove].any? { |h| h && !h.empty? }
96
+ update_retro_file(retro, set: set, add: add, remove: remove) if has_field_updates
97
+
98
+ # Apply move if requested
99
+ current_path = retro.path
100
+ current_special = retro.special_folder
101
+ if move_to
102
+ mover = Molecules::RetroMover.new(@root_dir)
103
+ new_path = if Ace::Support::Items::Atoms::SpecialFolderDetector.move_to_root?(move_to)
104
+ mover.move_to_root(retro)
105
+ else
106
+ archive_date = parse_archive_date(retro)
107
+ mover.move(retro, to: move_to, date: archive_date)
108
+ end
109
+ current_path = new_path
110
+ current_special = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
111
+ new_path, root: @root_dir
112
+ )
113
+ end
114
+
115
+ # Reload and return updated retro
116
+ loader.load(current_path, id: retro.id, special_folder: current_special)
117
+ end
118
+
119
+ # Get the root directory
120
+ # @return [String] Absolute path to retros root
121
+ attr_reader :root_dir
122
+
123
+ private
124
+
125
+ def load_config
126
+ gem_root = File.expand_path("../../../..", __dir__)
127
+ # lib/ace/retro/organisms/ → 4 levels up to gem root
128
+ Molecules::RetroConfigLoader.load(gem_root: gem_root)
129
+ end
130
+
131
+ def resolve_root_dir
132
+ Molecules::RetroConfigLoader.root_dir(@config)
133
+ end
134
+
135
+ def ensure_root_dir
136
+ require "fileutils"
137
+ FileUtils.mkdir_p(@root_dir) unless Dir.exist?(@root_dir)
138
+ end
139
+
140
+ def resolve_scan_result(ref)
141
+ resolver = Molecules::RetroResolver.new(@root_dir)
142
+ resolver.resolve(ref)
143
+ end
144
+
145
+ def filter_by_tags(retros, tags)
146
+ return retros if tags.empty?
147
+
148
+ retros.select do |retro|
149
+ tags.any? { |tag| retro.tags.include?(tag) }
150
+ end
151
+ end
152
+
153
+ def update_retro_file(retro, set:, add:, remove:)
154
+ content = File.read(retro.file_path)
155
+ frontmatter, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
156
+ # Strip leading newline from body so rebuild doesn't double-space
157
+ body = body.sub(/\A\n/, "")
158
+
159
+ # Apply set operations
160
+ set.each { |k, v| frontmatter[k.to_s] = v }
161
+
162
+ # Apply add operations (for arrays)
163
+ add.each do |k, v|
164
+ key = k.to_s
165
+ current = Array(frontmatter[key])
166
+ values = Array(v)
167
+ frontmatter[key] = (current + values).uniq
168
+ end
169
+
170
+ # Apply remove operations (for arrays)
171
+ remove.each do |k, v|
172
+ key = k.to_s
173
+ next unless frontmatter[key].is_a?(Array)
174
+
175
+ values = Array(v)
176
+ frontmatter[key] = frontmatter[key] - values
177
+ end
178
+
179
+ # Write back atomically (temp + rename to avoid partial writes)
180
+ new_content = Ace::Support::Items::Atoms::FrontmatterSerializer.rebuild(frontmatter, body)
181
+ tmp_path = "#{retro.file_path}.tmp.#{Process.pid}"
182
+ File.write(tmp_path, new_content)
183
+ File.rename(tmp_path, retro.file_path)
184
+ ensure
185
+ begin
186
+ File.unlink(tmp_path) if tmp_path && File.exist?(tmp_path)
187
+ rescue
188
+ nil
189
+ end
190
+ end
191
+
192
+ # Extract archive date from retro frontmatter, falling back to Time.now
193
+ def parse_archive_date(retro)
194
+ raw = retro.metadata["completed_at"] || retro.created_at
195
+ return nil unless raw
196
+
197
+ case raw
198
+ when Time then raw
199
+ when DateTime then raw.to_time
200
+ else begin
201
+ Time.parse(raw.to_s)
202
+ rescue
203
+ nil
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Retro
5
+ VERSION = "0.16.0"
6
+ end
7
+ end
data/lib/ace/retro.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "retro/version"
4
+
5
+ # External dependencies
6
+ require "ace/b36ts"
7
+ require "ace/support/items"
8
+
9
+ # Atoms
10
+ require_relative "retro/atoms/retro_id_formatter"
11
+ require_relative "retro/atoms/retro_file_pattern"
12
+ require_relative "retro/atoms/retro_frontmatter_defaults"
13
+ require_relative "retro/atoms/retro_validation_rules"
14
+
15
+ # Models
16
+ require_relative "retro/models/retro"
17
+
18
+ # Molecules
19
+ require_relative "retro/molecules/retro_config_loader"
20
+ require_relative "retro/molecules/retro_scanner"
21
+ require_relative "retro/molecules/retro_resolver"
22
+ require_relative "retro/molecules/retro_loader"
23
+ require_relative "retro/molecules/retro_creator"
24
+ require_relative "retro/molecules/retro_mover"
25
+ require_relative "retro/molecules/retro_display_formatter"
26
+ require_relative "retro/molecules/retro_frontmatter_validator"
27
+ require_relative "retro/molecules/retro_structure_validator"
28
+ require_relative "retro/molecules/retro_doctor_fixer"
29
+ require_relative "retro/molecules/retro_doctor_reporter"
30
+
31
+ # Organisms
32
+ require_relative "retro/organisms/retro_manager"
33
+ require_relative "retro/organisms/retro_doctor"
34
+
35
+ module Ace
36
+ # Retro management gem for ACE.
37
+ # Manages retrospectives in .ace-retros/ using raw 6-char b36ts IDs.
38
+ module Retro
39
+ class Error < StandardError; end
40
+ end
41
+ end