ace-support-items 0.15.3

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +174 -0
  3. data/README.md +29 -0
  4. data/Rakefile +13 -0
  5. data/lib/ace/support/items/atoms/ansi_colors.rb +37 -0
  6. data/lib/ace/support/items/atoms/date_partition_path.rb +24 -0
  7. data/lib/ace/support/items/atoms/field_argument_parser.rb +107 -0
  8. data/lib/ace/support/items/atoms/filter_parser.rb +79 -0
  9. data/lib/ace/support/items/atoms/folder_completion_detector.rb +40 -0
  10. data/lib/ace/support/items/atoms/frontmatter_parser.rb +57 -0
  11. data/lib/ace/support/items/atoms/frontmatter_serializer.rb +81 -0
  12. data/lib/ace/support/items/atoms/item_id_formatter.rb +81 -0
  13. data/lib/ace/support/items/atoms/item_id_parser.rb +89 -0
  14. data/lib/ace/support/items/atoms/item_statistics.rb +26 -0
  15. data/lib/ace/support/items/atoms/position_generator.rb +64 -0
  16. data/lib/ace/support/items/atoms/relative_time_formatter.rb +48 -0
  17. data/lib/ace/support/items/atoms/slug_sanitizer.rb +67 -0
  18. data/lib/ace/support/items/atoms/sort_score_calculator.rb +54 -0
  19. data/lib/ace/support/items/atoms/special_folder_detector.rb +101 -0
  20. data/lib/ace/support/items/atoms/stats_line_formatter.rb +85 -0
  21. data/lib/ace/support/items/atoms/title_extractor.rb +22 -0
  22. data/lib/ace/support/items/models/item_id.rb +55 -0
  23. data/lib/ace/support/items/models/loaded_document.rb +28 -0
  24. data/lib/ace/support/items/models/scan_result.rb +32 -0
  25. data/lib/ace/support/items/molecules/base_formatter.rb +51 -0
  26. data/lib/ace/support/items/molecules/directory_scanner.rb +106 -0
  27. data/lib/ace/support/items/molecules/document_loader.rb +77 -0
  28. data/lib/ace/support/items/molecules/field_updater.rb +120 -0
  29. data/lib/ace/support/items/molecules/filter_applier.rb +117 -0
  30. data/lib/ace/support/items/molecules/folder_mover.rb +91 -0
  31. data/lib/ace/support/items/molecules/git_committer.rb +21 -0
  32. data/lib/ace/support/items/molecules/item_sorter.rb +73 -0
  33. data/lib/ace/support/items/molecules/llm_slug_generator.rb +264 -0
  34. data/lib/ace/support/items/molecules/shortcut_resolver.rb +87 -0
  35. data/lib/ace/support/items/molecules/smart_sorter.rb +39 -0
  36. data/lib/ace/support/items/molecules/status_categorizer.rb +60 -0
  37. data/lib/ace/support/items/version.rb +9 -0
  38. data/lib/ace/support/items.rb +50 -0
  39. metadata +111 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Items
6
+ module Molecules
7
+ # Shells out to ace-git-commit for auto-committing after mutations.
8
+ # Pure CLI invocation — no gem dependency on ace-git-commit.
9
+ class GitCommitter
10
+ # @param paths [Array<String>] File/directory paths to commit
11
+ # @param intention [String] Commit intention for LLM message generation
12
+ # @return [Boolean] true if commit succeeded
13
+ def self.commit(paths:, intention:)
14
+ cmd = ["ace-git-commit"] + paths + ["-i", intention]
15
+ system(*cmd)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Items
6
+ module Molecules
7
+ # Sorts collections of items by a field with configurable direction.
8
+ # Nil values sort last regardless of direction.
9
+ class ItemSorter
10
+ # Sort items by a field
11
+ # @param items [Array] Items to sort
12
+ # @param field [String, Symbol] Field name to sort by
13
+ # @param direction [Symbol] :asc or :desc (default: :asc)
14
+ # @param value_accessor [Proc, nil] Custom accessor: ->(item, key) { value }
15
+ # @return [Array] Sorted items
16
+ def self.sort(items, field:, direction: :asc, value_accessor: nil)
17
+ return [] if items.nil? || items.empty?
18
+
19
+ accessor = value_accessor || method(:default_value_accessor)
20
+
21
+ multiplier = (direction == :desc) ? -1 : 1
22
+
23
+ items.sort do |a, b|
24
+ val_a = accessor.call(a, field.to_s)
25
+ val_b = accessor.call(b, field.to_s)
26
+
27
+ # Nil values sort last regardless of direction
28
+ if val_a.nil? && val_b.nil?
29
+ 0
30
+ elsif val_a.nil?
31
+ 1
32
+ elsif val_b.nil?
33
+ -1
34
+ else
35
+ (val_a <=> val_b || 0) * multiplier
36
+ end
37
+ end
38
+ end
39
+
40
+ # Default value accessor matching FilterApplier convention
41
+ private_class_method def self.default_value_accessor(item, key)
42
+ if item.respond_to?(:[])
43
+ val = begin
44
+ item[key.to_sym]
45
+ rescue
46
+ nil
47
+ end
48
+ return val unless val.nil?
49
+
50
+ val = begin
51
+ item[key.to_s]
52
+ rescue
53
+ nil
54
+ end
55
+ return val unless val.nil?
56
+ end
57
+
58
+ if item.respond_to?(key.to_sym)
59
+ return item.send(key.to_sym)
60
+ end
61
+
62
+ if item.respond_to?(:frontmatter) && item.frontmatter.is_a?(Hash)
63
+ val = item.frontmatter[key.to_s] || item.frontmatter[key.to_sym]
64
+ return val unless val.nil?
65
+ end
66
+
67
+ nil
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Support
7
+ module Items
8
+ module Molecules
9
+ # Generates hierarchical slugs using LLM with fallback to deterministic generation.
10
+ # Soft dependency on ace-llm — gracefully falls back if not available.
11
+ class LlmSlugGenerator
12
+ # Goal type keywords for consistent naming
13
+ GOAL_TYPES = %w[add enhance fix refactor].freeze
14
+
15
+ def initialize(debug: false)
16
+ @debug = debug
17
+ end
18
+
19
+ # Generate hierarchical slugs for a task
20
+ # @param title [String] Task title
21
+ # @param context [Hash] Additional context (project_name, type, etc.)
22
+ # @return [Hash] { folder_slug:, file_slug:, success:, source: } or fallback result
23
+ def generate_task_slugs(title, context = {})
24
+ llm_result = try_llm_task_generation(title, context)
25
+ return llm_result if llm_result[:success]
26
+
27
+ debug_log("LLM generation failed, using fallback")
28
+ fallback_task_generation(title, context)
29
+ end
30
+
31
+ # Generate hierarchical slugs for an idea
32
+ # @param description [String] Idea description/content
33
+ # @param context [Hash] Additional context
34
+ # @return [Hash] { folder_slug:, file_slug:, success:, source: } or fallback result
35
+ def generate_idea_slugs(description, context = {})
36
+ llm_result = try_llm_idea_generation(description, context)
37
+ return llm_result if llm_result[:success]
38
+
39
+ debug_log("LLM generation failed, using fallback")
40
+ fallback_idea_generation(description, context)
41
+ end
42
+
43
+ private
44
+
45
+ def load_slug_prompt
46
+ require "open3"
47
+
48
+ stdout, _stderr, status = Open3.capture3("ace-nav", "prompt://slug-generation", "--content")
49
+
50
+ if status.success?
51
+ debug_log("Successfully loaded slug-generation prompt via ace-nav")
52
+ stdout.strip
53
+ else
54
+ raise "Slug generation prompt not found via ace-nav"
55
+ end
56
+ rescue => e
57
+ debug_log("Error loading slug prompt: #{e.message}")
58
+ raise "Failed to load slug generation prompt: #{e.message}"
59
+ end
60
+
61
+ def try_llm_task_generation(title, context)
62
+ prompt = build_task_slug_prompt(title, context)
63
+ result = call_llm(prompt)
64
+ return {success: false} unless result[:success]
65
+
66
+ parsed = parse_llm_response(result[:text])
67
+ return {success: false} unless parsed
68
+
69
+ if valid_slugs?(parsed)
70
+ {
71
+ success: true,
72
+ folder_slug: parsed["folder_slug"],
73
+ file_slug: parsed["file_slug"],
74
+ source: :llm
75
+ }
76
+ else
77
+ debug_log("LLM returned invalid slug format: #{parsed.inspect}")
78
+ {success: false}
79
+ end
80
+ rescue => e
81
+ debug_log("LLM task generation error: #{e.message}")
82
+ {success: false}
83
+ end
84
+
85
+ def try_llm_idea_generation(description, context)
86
+ prompt = build_idea_slug_prompt(description, context)
87
+ result = call_llm(prompt)
88
+ return {success: false} unless result[:success]
89
+
90
+ parsed = parse_llm_response(result[:text])
91
+ return {success: false} unless parsed
92
+
93
+ if valid_slugs?(parsed)
94
+ {
95
+ success: true,
96
+ folder_slug: parsed["folder_slug"],
97
+ file_slug: parsed["file_slug"],
98
+ source: :llm
99
+ }
100
+ else
101
+ debug_log("LLM returned invalid slug format: #{parsed.inspect}")
102
+ {success: false}
103
+ end
104
+ rescue => e
105
+ debug_log("LLM idea generation error: #{e.message}")
106
+ {success: false}
107
+ end
108
+
109
+ def call_llm(prompt)
110
+ # Soft dependency on ace-llm
111
+ require "ace/llm/query_interface"
112
+ require "ace/llm/molecules/llm_alias_resolver"
113
+
114
+ debug_log("=== LLM PROMPT ===")
115
+ debug_log(prompt)
116
+ debug_log("=== END PROMPT ===")
117
+
118
+ response = Ace::LLM::QueryInterface.query(
119
+ "glite",
120
+ prompt,
121
+ temperature: 0.3,
122
+ max_tokens: 500,
123
+ debug: @debug
124
+ )
125
+
126
+ debug_log("=== LLM RESPONSE ===")
127
+ debug_log(response[:text])
128
+ debug_log("=== END RESPONSE ===")
129
+
130
+ {success: true, text: response[:text]}
131
+ rescue LoadError => e
132
+ debug_log("ace-llm not available: #{e.message}")
133
+ {success: false, error: e.message}
134
+ rescue => e
135
+ debug_log("LLM call failed: #{e.message}")
136
+ {success: false, error: e.message}
137
+ end
138
+
139
+ def parse_llm_response(text)
140
+ json_text = text.strip
141
+ json_text = json_text.gsub(/^```json\s*\n?/, "").gsub(/\n?```$/, "") if json_text.include?("```")
142
+
143
+ JSON.parse(json_text)
144
+ rescue JSON::ParserError => e
145
+ debug_log("Failed to parse LLM JSON response: #{e.message}")
146
+ nil
147
+ end
148
+
149
+ def build_task_slug_prompt(title, context)
150
+ prompt_template = load_slug_prompt
151
+
152
+ <<~PROMPT
153
+ #{prompt_template}
154
+
155
+ ---
156
+
157
+ ## Task Details
158
+
159
+ **Task Title**: #{title}
160
+ **Additional Context**: #{context.to_json}
161
+
162
+ Generate the hierarchical slugs for this task.
163
+ PROMPT
164
+ end
165
+
166
+ def build_idea_slug_prompt(description, context)
167
+ prompt_template = load_slug_prompt
168
+ desc_preview = description[0..1000]
169
+
170
+ <<~PROMPT
171
+ #{prompt_template}
172
+
173
+ ---
174
+
175
+ ## Idea Details
176
+
177
+ **Idea Description**: #{desc_preview}
178
+ **Additional Context**: #{context.to_json}
179
+
180
+ Generate the hierarchical slugs for this idea.
181
+ PROMPT
182
+ end
183
+
184
+ def valid_slugs?(parsed)
185
+ return false unless parsed.is_a?(Hash)
186
+ return false unless parsed["folder_slug"].is_a?(String)
187
+ return false unless parsed["file_slug"].is_a?(String)
188
+ return false if parsed["folder_slug"].empty?
189
+ return false if parsed["file_slug"].empty?
190
+
191
+ folder_valid = parsed["folder_slug"] =~ /^[a-z0-9]+(-[a-z0-9]+)*$/
192
+ file_valid = parsed["file_slug"] =~ /^[a-z0-9]+(-[a-z0-9]+)*$/
193
+
194
+ folder_valid && file_valid
195
+ end
196
+
197
+ def fallback_task_generation(title, _context)
198
+ slug = sanitize_to_slug(title)
199
+ parts = slug.split("-")
200
+ folder_parts = parts.take(3)
201
+ file_parts = parts.drop(3)
202
+
203
+ folder_slug = folder_parts.join("-")
204
+ file_slug = file_parts.any? ? file_parts.join("-") : folder_slug
205
+
206
+ {
207
+ success: true,
208
+ folder_slug: folder_slug,
209
+ file_slug: file_slug,
210
+ source: :fallback
211
+ }
212
+ end
213
+
214
+ def fallback_idea_generation(description, _context)
215
+ title = description.split("\n").first || description
216
+ title = title[0..49] if title.length > 50
217
+ file_slug = sanitize_to_slug(title)
218
+
219
+ area = extract_area_from_content(description)
220
+ goal_type = extract_goal_type_from_content(description)
221
+ folder_slug = [area, goal_type].compact.join("-")
222
+ folder_slug = file_slug if folder_slug.empty?
223
+
224
+ {
225
+ success: true,
226
+ folder_slug: folder_slug,
227
+ file_slug: file_slug,
228
+ source: :fallback
229
+ }
230
+ end
231
+
232
+ def sanitize_to_slug(text)
233
+ text.to_s.downcase
234
+ .gsub(/[^\w\s-]/, "")
235
+ .gsub(/[\s_]+/, "-").squeeze("-")
236
+ .gsub(/^-|-$/, "")
237
+ .strip
238
+ end
239
+
240
+ def extract_area_from_content(content)
241
+ content_lower = content.downcase
242
+ areas = %w[taskflow search docs git llm nav review lint test]
243
+ areas.find { |area| content_lower.include?(area) }
244
+ end
245
+
246
+ def extract_goal_type_from_content(content)
247
+ content_lower = content.downcase
248
+
249
+ return "add" if /\b(add|create|implement|new)\b/.match?(content_lower)
250
+ return "enhance" if /\b(enhance|improve|update|upgrade)\b/.match?(content_lower)
251
+ return "fix" if /\b(fix|repair|resolve|correct)\b/.match?(content_lower)
252
+ return "refactor" if /\b(refactor|restructure|reorganize)\b/.match?(content_lower)
253
+
254
+ "enhance"
255
+ end
256
+
257
+ def debug_log(message)
258
+ warn "[LlmSlugGenerator] #{message}" if @debug
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Items
6
+ module Molecules
7
+ # Resolves shortcut references to item ScanResult objects.
8
+ #
9
+ # Shortcuts are the last N characters of an item ID.
10
+ # For ideas (6-char IDs): "8ppq7w" => shortcut "q7w"
11
+ # For tasks (9-char formatted IDs): "8pp.t.q7w" => shortcut "q7w"
12
+ # Full IDs are also accepted directly.
13
+ #
14
+ # Warns (via callback or STDERR) when multiple matches are found.
15
+ class ShortcutResolver
16
+ # @param scan_results [Array<ScanResult>] Scan results to resolve against
17
+ # @param full_id_length [Integer] Length of a full ID (6 for raw b36ts, 9 for type-marked)
18
+ def initialize(scan_results, full_id_length: 6)
19
+ @scan_results = scan_results
20
+ @full_id_length = full_id_length
21
+ end
22
+
23
+ # Resolve a reference to a single ScanResult
24
+ # @param ref [String] Full ID or suffix shortcut
25
+ # @param on_ambiguity [Proc, nil] Called with array of matches on ambiguity
26
+ # @return [ScanResult, nil] The resolved result, or nil if not found
27
+ def resolve(ref, on_ambiguity: nil)
28
+ return nil if ref.nil? || ref.empty?
29
+
30
+ ref = ref.strip.downcase
31
+
32
+ if ref.length == @full_id_length
33
+ # Full ID match
34
+ exact = @scan_results.find { |r| r.id == ref }
35
+ return exact
36
+ end
37
+
38
+ # Suffix match (last N characters)
39
+ matches = @scan_results.select { |r| r.id.end_with?(ref) }
40
+
41
+ if matches.empty?
42
+ nil
43
+ elsif matches.size == 1
44
+ matches.first
45
+ else
46
+ # Ambiguity: multiple matches
47
+ if on_ambiguity
48
+ on_ambiguity.call(matches)
49
+ else
50
+ warn "Warning: Ambiguous shortcut '#{ref}' matches #{matches.size} items: " \
51
+ "#{matches.map(&:id).join(", ")}. Using most recent."
52
+ end
53
+ # Return most recent (last by sorted ID = chronologically latest)
54
+ matches.last
55
+ end
56
+ end
57
+
58
+ # Check if a reference would be ambiguous
59
+ # @param ref [String] Reference to check
60
+ # @return [Boolean] True if multiple matches exist
61
+ def ambiguous?(ref)
62
+ return false if ref.nil? || ref.length == @full_id_length
63
+
64
+ ref = ref.strip.downcase
65
+ matches = @scan_results.select { |r| r.id.end_with?(ref) }
66
+ matches.size > 1
67
+ end
68
+
69
+ # Return all matches for a reference (useful for listing ambiguous matches)
70
+ # @param ref [String] Reference to resolve
71
+ # @return [Array<ScanResult>] All matching results
72
+ def all_matches(ref)
73
+ return [] if ref.nil? || ref.empty?
74
+
75
+ ref = ref.strip.downcase
76
+
77
+ if ref.length == @full_id_length
78
+ @scan_results.select { |r| r.id == ref }
79
+ else
80
+ @scan_results.select { |r| r.id.end_with?(ref) }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Items
6
+ module Molecules
7
+ # Sorts items using pinned-first + auto-sort logic.
8
+ # Pinned items (those with a position value) sort first by position ascending.
9
+ # Unpinned items sort second by computed score descending.
10
+ class SmartSorter
11
+ # @param items [Array] Items to sort
12
+ # @param score_fn [Proc] ->(item) { Float } computes auto-sort score
13
+ # @param pin_accessor [Proc] ->(item) { String|nil } reads position field
14
+ # @return [Array] Pinned items (by position asc) + unpinned items (by score desc)
15
+ def self.sort(items, score_fn:, pin_accessor:)
16
+ return [] if items.nil? || items.empty?
17
+
18
+ pinned = []
19
+ unpinned = []
20
+
21
+ items.each do |item|
22
+ pos = pin_accessor.call(item)
23
+ if pos && !pos.to_s.empty?
24
+ pinned << item
25
+ else
26
+ unpinned << item
27
+ end
28
+ end
29
+
30
+ sorted_pinned = pinned.sort_by { |item| pin_accessor.call(item).to_s }
31
+ sorted_unpinned = unpinned.sort_by { |item| -score_fn.call(item) }
32
+
33
+ sorted_pinned + sorted_unpinned
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Items
6
+ module Molecules
7
+ # Categorizes items into "up next" and "recently done" buckets
8
+ # for status overview displays. Generic — works with any item
9
+ # responding to :status, :file_path, :special_folder, :id.
10
+ class StatusCategorizer
11
+ # @param items [Array] All loaded items
12
+ # @param up_next_limit [Integer] Max up-next items (0 = disable section)
13
+ # @param recently_done_limit [Integer] Max recently-done items (0 = disable section)
14
+ # @param pending_statuses [Array<String>] Statuses considered "up next"
15
+ # @param done_statuses [Array<String>] Statuses considered "recently done"
16
+ # @return [Hash] { up_next: [...], recently_done: [{ item:, completed_at: }] }
17
+ # @param up_next_sorter [Proc, nil] Custom sorter for up-next items.
18
+ # Receives array of items, returns sorted array. Default: sort by id.
19
+ def self.categorize(items, up_next_limit:, recently_done_limit:,
20
+ pending_statuses: ["pending"], done_statuses: ["done"],
21
+ up_next_sorter: nil)
22
+ up_next = if up_next_limit > 0
23
+ candidates = items
24
+ .select { |i| pending_statuses.include?(i.status) && i.special_folder.nil? }
25
+ sorted = if up_next_sorter
26
+ up_next_sorter.call(candidates)
27
+ else
28
+ candidates.sort_by(&:id)
29
+ end
30
+ sorted.first(up_next_limit)
31
+ else
32
+ []
33
+ end
34
+
35
+ recently_done = if recently_done_limit > 0
36
+ items
37
+ .select { |i| done_statuses.include?(i.status) }
38
+ .map { |i| {item: i, completed_at: safe_mtime(i.file_path)} }
39
+ .sort_by { |entry| -(entry[:completed_at]&.to_f || 0) }
40
+ .first(recently_done_limit)
41
+ else
42
+ []
43
+ end
44
+
45
+ {up_next: up_next, recently_done: recently_done}
46
+ end
47
+
48
+ # Safely read file mtime, returning nil if the file is missing.
49
+ def self.safe_mtime(path)
50
+ File.mtime(path)
51
+ rescue Errno::ENOENT, TypeError
52
+ nil
53
+ end
54
+
55
+ private_class_method :safe_mtime
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Items
6
+ VERSION = "0.15.3"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "items/version"
4
+
5
+ # Atoms
6
+ require_relative "items/atoms/ansi_colors"
7
+ require_relative "items/atoms/slug_sanitizer"
8
+ require_relative "items/atoms/field_argument_parser"
9
+ require_relative "items/atoms/special_folder_detector"
10
+ require_relative "items/atoms/folder_completion_detector"
11
+ require_relative "items/atoms/frontmatter_parser"
12
+ require_relative "items/atoms/frontmatter_serializer"
13
+ require_relative "items/atoms/filter_parser"
14
+ require_relative "items/atoms/title_extractor"
15
+ require_relative "items/atoms/date_partition_path"
16
+ require_relative "items/atoms/item_id_formatter"
17
+ require_relative "items/atoms/item_id_parser"
18
+ require_relative "items/atoms/item_statistics"
19
+ require_relative "items/atoms/stats_line_formatter"
20
+ require_relative "items/atoms/relative_time_formatter"
21
+ require_relative "items/atoms/sort_score_calculator"
22
+ require_relative "items/atoms/position_generator"
23
+
24
+ # Models
25
+ require_relative "items/models/scan_result"
26
+ require_relative "items/models/loaded_document"
27
+ require_relative "items/models/item_id"
28
+
29
+ # Molecules
30
+ require_relative "items/molecules/directory_scanner"
31
+ require_relative "items/molecules/shortcut_resolver"
32
+ require_relative "items/molecules/document_loader"
33
+ require_relative "items/molecules/filter_applier"
34
+ require_relative "items/molecules/item_sorter"
35
+ require_relative "items/molecules/base_formatter"
36
+ require_relative "items/molecules/field_updater"
37
+ require_relative "items/molecules/folder_mover"
38
+ require_relative "items/molecules/llm_slug_generator"
39
+ require_relative "items/molecules/status_categorizer"
40
+ require_relative "items/molecules/git_committer"
41
+ require_relative "items/molecules/smart_sorter"
42
+
43
+ module Ace
44
+ module Support
45
+ # Items provides shared infrastructure for item management (tasks, ideas, etc.)
46
+ # across ace-* gems. Built on b36ts-based IDs and folder conventions.
47
+ module Items
48
+ end
49
+ end
50
+ end