ace-git-worktree 0.19.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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/git/worktree.yml +250 -0
  3. data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +19 -0
  4. data/CHANGELOG.md +957 -0
  5. data/LICENSE +21 -0
  6. data/README.md +40 -0
  7. data/Rakefile +14 -0
  8. data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
  9. data/docs/demo/ace-git-worktree-getting-started.tape.yml +28 -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 +114 -0
  13. data/docs/handbook.md +38 -0
  14. data/docs/usage.md +334 -0
  15. data/exe/ace-git-worktree +24 -0
  16. data/handbook/agents/worktree.ag.md +189 -0
  17. data/handbook/skills/as-git-worktree/SKILL.md +27 -0
  18. data/handbook/skills/as-git-worktree-create/SKILL.md +21 -0
  19. data/handbook/skills/as-git-worktree-manage/SKILL.md +20 -0
  20. data/handbook/workflow-instructions/git/worktree-create.wf.md +262 -0
  21. data/handbook/workflow-instructions/git/worktree-manage.wf.md +384 -0
  22. data/handbook/workflow-instructions/git/worktree.wf.md +224 -0
  23. data/lib/ace/git/worktree/atoms/git_command.rb +121 -0
  24. data/lib/ace/git/worktree/atoms/path_expander.rb +189 -0
  25. data/lib/ace/git/worktree/atoms/slug_generator.rb +235 -0
  26. data/lib/ace/git/worktree/atoms/task_id_extractor.rb +91 -0
  27. data/lib/ace/git/worktree/cli/commands/config.rb +50 -0
  28. data/lib/ace/git/worktree/cli/commands/create.rb +80 -0
  29. data/lib/ace/git/worktree/cli/commands/list.rb +76 -0
  30. data/lib/ace/git/worktree/cli/commands/prune.rb +43 -0
  31. data/lib/ace/git/worktree/cli/commands/remove.rb +48 -0
  32. data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +66 -0
  33. data/lib/ace/git/worktree/cli/commands/switch.rb +44 -0
  34. data/lib/ace/git/worktree/cli.rb +103 -0
  35. data/lib/ace/git/worktree/commands/config_command.rb +351 -0
  36. data/lib/ace/git/worktree/commands/create_command.rb +961 -0
  37. data/lib/ace/git/worktree/commands/list_command.rb +247 -0
  38. data/lib/ace/git/worktree/commands/prune_command.rb +260 -0
  39. data/lib/ace/git/worktree/commands/remove_command.rb +522 -0
  40. data/lib/ace/git/worktree/commands/switch_command.rb +249 -0
  41. data/lib/ace/git/worktree/configuration.rb +167 -0
  42. data/lib/ace/git/worktree/models/worktree_config.rb +502 -0
  43. data/lib/ace/git/worktree/models/worktree_info.rb +303 -0
  44. data/lib/ace/git/worktree/models/worktree_metadata.rb +294 -0
  45. data/lib/ace/git/worktree/molecules/config_loader.rb +125 -0
  46. data/lib/ace/git/worktree/molecules/current_task_linker.rb +136 -0
  47. data/lib/ace/git/worktree/molecules/hook_executor.rb +361 -0
  48. data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +186 -0
  49. data/lib/ace/git/worktree/molecules/pr_creator.rb +253 -0
  50. data/lib/ace/git/worktree/molecules/task_committer.rb +329 -0
  51. data/lib/ace/git/worktree/molecules/task_fetcher.rb +244 -0
  52. data/lib/ace/git/worktree/molecules/task_pusher.rb +183 -0
  53. data/lib/ace/git/worktree/molecules/task_status_updater.rb +447 -0
  54. data/lib/ace/git/worktree/molecules/worktree_creator.rb +832 -0
  55. data/lib/ace/git/worktree/molecules/worktree_lister.rb +337 -0
  56. data/lib/ace/git/worktree/molecules/worktree_remover.rb +416 -0
  57. data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +906 -0
  58. data/lib/ace/git/worktree/organisms/worktree_manager.rb +714 -0
  59. data/lib/ace/git/worktree/version.rb +9 -0
  60. data/lib/ace/git/worktree.rb +215 -0
  61. metadata +218 -0
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../atoms/git_command"
4
+
5
+ module Ace
6
+ module Git
7
+ module Worktree
8
+ module Molecules
9
+ # Task pusher molecule
10
+ #
11
+ # Pushes task changes to remote repository after commits.
12
+ # Used by TaskWorktreeOrchestrator to ensure task status updates
13
+ # are visible in PRs.
14
+ #
15
+ # @example Push to default remote
16
+ # pusher = TaskPusher.new
17
+ # result = pusher.push
18
+ #
19
+ # @example Push to specific remote and branch
20
+ # result = pusher.push(remote: "upstream", branch: "feature-branch")
21
+ class TaskPusher
22
+ # Default timeout for git push commands
23
+ DEFAULT_TIMEOUT = 60
24
+
25
+ # Initialize a new TaskPusher
26
+ #
27
+ # @param timeout [Integer] Command timeout in seconds
28
+ def initialize(timeout: DEFAULT_TIMEOUT)
29
+ @timeout = timeout
30
+ end
31
+
32
+ # Push current branch to remote
33
+ #
34
+ # @param remote [String] Remote name (default: "origin")
35
+ # @param branch [String, nil] Branch name (default: current branch)
36
+ # @param set_upstream [Boolean] Set upstream tracking (default: true)
37
+ # @return [Hash] Result with :success, :output, :error
38
+ #
39
+ # @example
40
+ # pusher = TaskPusher.new
41
+ # result = pusher.push(remote: "origin")
42
+ # result[:success] # => true
43
+ def push(remote: "origin", branch: nil, set_upstream: true)
44
+ branch ||= current_branch
45
+ return failure_result("Could not determine current branch") unless branch
46
+
47
+ args = ["push"]
48
+ args << "-u" if set_upstream
49
+ args << remote
50
+ args << branch
51
+
52
+ result = Atoms::GitCommand.execute(*args, timeout: @timeout)
53
+
54
+ {
55
+ success: result[:success],
56
+ output: result[:output],
57
+ error: result[:error],
58
+ remote: remote,
59
+ branch: branch
60
+ }
61
+ end
62
+
63
+ # Check if remote exists
64
+ #
65
+ # @param remote [String] Remote name to check
66
+ # @return [Boolean] true if remote exists
67
+ #
68
+ # @example
69
+ # pusher.remote_exists?("origin") # => true
70
+ def remote_exists?(remote)
71
+ result = Atoms::GitCommand.execute("remote", "get-url", remote, timeout: 5)
72
+ result[:success]
73
+ end
74
+
75
+ # Get current branch name
76
+ #
77
+ # @return [String, nil] Current branch name or nil if detached
78
+ #
79
+ # @example
80
+ # pusher.current_branch # => "feature-branch"
81
+ def current_branch
82
+ result = Atoms::GitCommand.execute("branch", "--show-current", timeout: 5)
83
+ return nil unless result[:success]
84
+
85
+ branch = result[:output]&.strip
86
+ branch.empty? ? nil : branch
87
+ end
88
+
89
+ # Check if branch has upstream tracking
90
+ #
91
+ # @param branch [String, nil] Branch to check (default: current)
92
+ # @return [Boolean] true if branch has upstream
93
+ #
94
+ # @example
95
+ # pusher.has_upstream? # => true
96
+ def has_upstream?(branch = nil)
97
+ branch ||= current_branch
98
+ return false unless branch
99
+
100
+ result = Atoms::GitCommand.execute(
101
+ "rev-parse", "--abbrev-ref", "#{branch}@{upstream}",
102
+ timeout: 5
103
+ )
104
+ result[:success]
105
+ end
106
+
107
+ # Set upstream tracking for a branch
108
+ #
109
+ # Uses `git branch --set-upstream-to` to configure tracking without pushing.
110
+ # Useful when the remote branch already exists or push is not desired.
111
+ #
112
+ # @param branch [String, nil] Branch to configure (default: current)
113
+ # @param remote [String] Remote name (default: "origin")
114
+ # @return [Hash] Result with :success, :output, :error, :remote, :branch
115
+ #
116
+ # @example
117
+ # pusher.set_upstream(branch: "feature", remote: "origin")
118
+ # # => { success: true, branch: "feature", remote: "origin", ... }
119
+ def set_upstream(branch: nil, remote: "origin")
120
+ branch ||= current_branch
121
+ return failure_result("Could not determine current branch") unless branch
122
+
123
+ result = Atoms::GitCommand.execute(
124
+ "branch", "--set-upstream-to=#{remote}/#{branch}", branch,
125
+ timeout: @timeout
126
+ )
127
+
128
+ {
129
+ success: result[:success],
130
+ output: result[:output],
131
+ error: result[:error],
132
+ remote: remote,
133
+ branch: branch
134
+ }
135
+ end
136
+
137
+ # Get the upstream remote/branch for current branch
138
+ #
139
+ # @param branch [String, nil] Branch to check (default: current)
140
+ # @return [Hash, nil] Hash with :remote and :branch keys, or nil
141
+ #
142
+ # @example
143
+ # pusher.get_upstream # => { remote: "origin", branch: "main" }
144
+ def get_upstream(branch = nil)
145
+ branch ||= current_branch
146
+ return nil unless branch
147
+
148
+ result = Atoms::GitCommand.execute(
149
+ "rev-parse", "--abbrev-ref", "#{branch}@{upstream}",
150
+ timeout: 5
151
+ )
152
+ return nil unless result[:success]
153
+
154
+ upstream = result[:output]&.strip
155
+ return nil if upstream.nil? || upstream.empty?
156
+
157
+ # Parse "origin/branch-name" format
158
+ parts = upstream.split("/", 2)
159
+ return nil if parts.length < 2
160
+
161
+ {remote: parts[0], branch: parts[1]}
162
+ end
163
+
164
+ private
165
+
166
+ # Create a failure result hash
167
+ #
168
+ # @param message [String] Error message
169
+ # @return [Hash] Failure result
170
+ def failure_result(message)
171
+ {
172
+ success: false,
173
+ output: "",
174
+ error: message,
175
+ remote: nil,
176
+ branch: nil
177
+ }
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,447 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Try to require ace-task API for direct integration (organism level only)
4
+ begin
5
+ require "ace/task/organisms/task_manager"
6
+ rescue LoadError
7
+ # ace-task not available - will fall back to CLI
8
+ end
9
+
10
+ require_relative "../atoms/task_id_extractor"
11
+
12
+ module Ace
13
+ module Git
14
+ module Worktree
15
+ module Molecules
16
+ # Task status updater molecule
17
+ #
18
+ # Updates task status using ace-task Ruby API or CLI commands.
19
+ # Provides methods for marking tasks as in-progress, done, etc.
20
+ # All methods return a result hash with :success and :message keys.
21
+ #
22
+ # @example Mark task as in-progress
23
+ # updater = TaskStatusUpdater.new
24
+ # result = updater.mark_in_progress("8pp.t.q7w")
25
+ # # => { success: true, message: "Task status updated to in-progress" }
26
+ #
27
+ # @example Update with custom status
28
+ # result = updater.update_status("8pp.t.q7w", "done")
29
+ class TaskStatusUpdater
30
+ # Default timeout for ace-task commands
31
+ DEFAULT_TIMEOUT = 10
32
+
33
+ # Initialize a new TaskStatusUpdater
34
+ #
35
+ # @param timeout [Integer] Command timeout in seconds
36
+ def initialize(timeout: DEFAULT_TIMEOUT)
37
+ @timeout = timeout
38
+ @project_root = ENV["PROJECT_ROOT_PATH"] || Dir.pwd
39
+ end
40
+
41
+ # Mark task as in-progress
42
+ #
43
+ # @param task_ref [String] Task reference
44
+ # @return [Hash] Result with :success and :message keys
45
+ def mark_in_progress(task_ref)
46
+ update_status(task_ref, "in-progress")
47
+ end
48
+
49
+ # Mark task as done
50
+ #
51
+ # @param task_ref [String] Task reference
52
+ # @return [Hash] Result with :success and :message keys
53
+ def mark_done(task_ref)
54
+ update_status(task_ref, "done")
55
+ end
56
+
57
+ # Mark task as blocked
58
+ #
59
+ # @param task_ref [String] Task reference
60
+ # @return [Hash] Result with :success and :message keys
61
+ def mark_blocked(task_ref)
62
+ update_status(task_ref, "blocked")
63
+ end
64
+
65
+ # Update task to custom status
66
+ #
67
+ # @param task_ref [String] Task reference
68
+ # @param status [String] New status value
69
+ # @return [Hash] Result with :success and :message keys
70
+ def update_status(task_ref, status)
71
+ return {success: false, message: "Task reference is required"} if task_ref.nil? || task_ref.empty?
72
+ return {success: false, message: "Status is required"} if status.nil? || status.empty?
73
+
74
+ puts "DEBUG: TaskStatusUpdater.update_status called with task_ref=#{task_ref}, status=#{status}" if ENV["DEBUG"]
75
+ puts "DEBUG: use_ruby_api? = #{use_ruby_api?}" if ENV["DEBUG"]
76
+
77
+ # Try Ruby API first (preferred in mono-repo)
78
+ if use_ruby_api?
79
+ puts "DEBUG: Using Ruby API for status update" if ENV["DEBUG"]
80
+ result = update_status_via_api(task_ref, status)
81
+ puts "DEBUG: Ruby API result: #{result}" if ENV["DEBUG"]
82
+ return result
83
+ end
84
+
85
+ # Fallback to CLI for standalone installations
86
+ puts "DEBUG: Using CLI for status update" if ENV["DEBUG"]
87
+ result = update_status_via_cli(task_ref, status)
88
+ puts "DEBUG: CLI result: #{result}" if ENV["DEBUG"]
89
+ result
90
+ end
91
+
92
+ # Update task priority
93
+ #
94
+ # @param task_ref [String] Task reference
95
+ # @param priority [String] New priority (high, medium, low)
96
+ # @return [Boolean] true if priority was updated successfully
97
+ def update_priority(task_ref, priority)
98
+ return false unless %w[high medium low].include?(priority.to_s)
99
+
100
+ normalized_ref = normalize_task_reference(task_ref)
101
+ return false unless normalized_ref
102
+
103
+ if use_ruby_api?
104
+ return update_fields_via_api(normalized_ref, {"priority" => priority})
105
+ end
106
+
107
+ result = execute_ace_task_command("update", normalized_ref, "--set", "priority=#{priority}")
108
+ result[:success]
109
+ end
110
+
111
+ # Update task estimate
112
+ #
113
+ # @param task_ref [String] Task reference
114
+ # @param estimate [String] New estimate (e.g., "2h", "1-2 days")
115
+ # @return [Boolean] true if estimate was updated successfully
116
+ def update_estimate(task_ref, estimate)
117
+ return false if estimate.nil? || estimate.empty?
118
+
119
+ normalized_ref = normalize_task_reference(task_ref)
120
+ return false unless normalized_ref
121
+
122
+ if use_ruby_api?
123
+ return update_fields_via_api(normalized_ref, {"estimate" => estimate})
124
+ end
125
+
126
+ result = execute_ace_task_command("update", normalized_ref, "--set", "estimate=#{estimate}")
127
+ result[:success]
128
+ end
129
+
130
+ # Add worktree metadata to task
131
+ #
132
+ # @param task_ref [String] Task reference
133
+ # @param worktree_metadata [WorktreeMetadata] Worktree metadata to add
134
+ # @return [Boolean] true if metadata was added successfully
135
+ def add_worktree_metadata(task_ref, worktree_metadata)
136
+ return false unless worktree_metadata.is_a?(Models::WorktreeMetadata)
137
+
138
+ # Try Ruby API first (preferred in mono-repo)
139
+ if use_ruby_api?
140
+ return add_worktree_metadata_via_api(task_ref, worktree_metadata)
141
+ end
142
+
143
+ # Fallback to CLI for standalone installations
144
+ add_worktree_metadata_via_cli(task_ref, worktree_metadata)
145
+ end
146
+
147
+ # Add PR metadata to task
148
+ #
149
+ # @param task_ref [String] Task reference
150
+ # @param pr_data [Hash] PR data with :number, :url, :created_at
151
+ # @return [Boolean] true if metadata was added successfully
152
+ def add_pr_metadata(task_ref, pr_data)
153
+ return false unless pr_data.is_a?(Hash)
154
+ return false unless pr_data[:number] && pr_data[:url]
155
+
156
+ normalized_ref = normalize_task_reference(task_ref)
157
+ return false unless normalized_ref
158
+
159
+ # Try Ruby API first (preferred in mono-repo)
160
+ if use_ruby_api?
161
+ return add_pr_metadata_via_api(task_ref, pr_data)
162
+ end
163
+
164
+ # Fallback to CLI for standalone installations
165
+ add_pr_metadata_via_cli(task_ref, pr_data)
166
+ end
167
+
168
+ # Add started_at timestamp to task
169
+ #
170
+ # @param task_ref [String] Task reference
171
+ # @return [Boolean] true if timestamp was added successfully
172
+ def add_started_at_timestamp(task_ref)
173
+ started_at = Time.now
174
+
175
+ normalized_ref = normalize_task_reference(task_ref)
176
+ return false unless normalized_ref
177
+
178
+ # Try Ruby API first (preferred in mono-repo)
179
+ if use_ruby_api?
180
+ return add_started_at_via_api(task_ref, started_at)
181
+ end
182
+
183
+ # Fallback to CLI for standalone installations
184
+ add_started_at_via_cli(task_ref, started_at)
185
+ end
186
+
187
+ # Remove worktree metadata from task
188
+ #
189
+ # @param task_ref [String] Task reference
190
+ # @return [Boolean] true if metadata was removed successfully
191
+ def remove_worktree_metadata(task_ref)
192
+ normalized_ref = normalize_task_reference(task_ref)
193
+ return false unless normalized_ref
194
+
195
+ false
196
+ end
197
+
198
+ # Get current task status
199
+ #
200
+ # @param task_ref [String] Task reference
201
+ # @return [String, nil] Current status or nil if not found
202
+ def get_status(task_ref)
203
+ normalized_ref = normalize_task_reference(task_ref)
204
+ return nil unless normalized_ref
205
+
206
+ # Fetch task metadata
207
+ fetcher = TaskFetcher.new
208
+ task = fetcher.fetch(normalized_ref)
209
+ task ? task[:status] : nil
210
+ end
211
+
212
+ # Check if ace-task update command is available
213
+ #
214
+ # @return [Boolean] true if update command is available
215
+ def update_command_available?
216
+ result = execute_ace_task_command("update", "--help", timeout: 5)
217
+ result[:success]
218
+ end
219
+
220
+ private
221
+
222
+ # Check if we can use Ruby API
223
+ #
224
+ # @return [Boolean] true if Ruby API is available
225
+ def use_ruby_api?
226
+ defined?(Ace::Task::Organisms::TaskManager)
227
+ end
228
+
229
+ # Update task status using Ruby API
230
+ #
231
+ # @param task_ref [String] Task reference
232
+ # @param status [String] New status
233
+ # @return [Hash] Result with :success and :message keys
234
+ def update_status_via_api(task_ref, status)
235
+ task_manager = Ace::Task::Organisms::TaskManager.new
236
+ result = task_manager.update(task_ref, set: {"status" => status})
237
+
238
+ puts "DEBUG: TaskManager result: #{result.inspect}" if ENV["DEBUG"]
239
+
240
+ if result
241
+ {success: true, message: "Task status updated to #{status}"}
242
+ else
243
+ puts "DEBUG: TaskManager returned nil" if ENV["DEBUG"]
244
+ {success: false, message: "Failed to update task status"}
245
+ end
246
+ rescue => e
247
+ puts "DEBUG: TaskManager exception: #{e.message}" if ENV["DEBUG"]
248
+ # Fall back to CLI on API error
249
+ update_status_via_cli(task_ref, status)
250
+ end
251
+
252
+ # Update task status using CLI
253
+ #
254
+ # @param task_ref [String] Task reference
255
+ # @param status [String] New status
256
+ # @return [Hash] Result with :success and :message keys
257
+ def update_status_via_cli(task_ref, status)
258
+ normalized_ref = normalize_task_reference(task_ref)
259
+ return {success: false, message: "Invalid task reference"} unless normalized_ref
260
+
261
+ result = execute_ace_task_command("update", normalized_ref, "--set", "status=#{status}")
262
+ if result[:success]
263
+ {success: true, message: "Task status updated to #{status}"}
264
+ else
265
+ {success: false, message: result[:error] || "Failed to update task status"}
266
+ end
267
+ end
268
+
269
+ # Update fields using Ruby API
270
+ #
271
+ # @param task_ref [String] Task reference
272
+ # @param fields [Hash] Fields to update
273
+ # @return [Boolean] true if successful
274
+ def update_fields_via_api(task_ref, fields)
275
+ task_manager = Ace::Task::Organisms::TaskManager.new
276
+ result = task_manager.update(task_ref, set: fields)
277
+ !result.nil?
278
+ rescue
279
+ false
280
+ end
281
+
282
+ # Add worktree metadata using Ruby API
283
+ #
284
+ # @param task_ref [String] Task reference
285
+ # @param worktree_metadata [WorktreeMetadata] Worktree metadata
286
+ # @return [Boolean] true if successful
287
+ def add_worktree_metadata_via_api(task_ref, worktree_metadata)
288
+ task_manager = Ace::Task::Organisms::TaskManager.new
289
+
290
+ # Convert worktree metadata to field updates
291
+ metadata_hash = worktree_metadata.to_h
292
+ field_updates = {}
293
+ metadata_hash.each do |field, value|
294
+ field_updates["worktree.#{field}"] = value.to_s
295
+ end
296
+
297
+ result = task_manager.update(task_ref, set: field_updates)
298
+ !result.nil?
299
+ rescue
300
+ # Fall back to CLI on API error
301
+ add_worktree_metadata_via_cli(task_ref, worktree_metadata)
302
+ end
303
+
304
+ # Add worktree metadata using CLI
305
+ #
306
+ # @param task_ref [String] Task reference
307
+ # @param worktree_metadata [WorktreeMetadata] Worktree metadata
308
+ # @return [Boolean] true if successful
309
+ def add_worktree_metadata_via_cli(task_ref, worktree_metadata)
310
+ normalized_ref = normalize_task_reference(task_ref)
311
+ return false unless normalized_ref
312
+
313
+ metadata_hash = worktree_metadata.to_h
314
+ success = true
315
+
316
+ metadata_hash.each do |field, value|
317
+ field_path = "worktree.#{field}"
318
+ result = execute_ace_task_command("update", normalized_ref, "--set", "#{field_path}=#{value}")
319
+ success &&= result[:success]
320
+ end
321
+
322
+ success
323
+ end
324
+
325
+ # Add PR metadata using Ruby API
326
+ #
327
+ # @param task_ref [String] Task reference
328
+ # @param pr_data [Hash] PR data
329
+ # @return [Boolean] true if successful
330
+ def add_pr_metadata_via_api(task_ref, pr_data)
331
+ task_manager = Ace::Task::Organisms::TaskManager.new
332
+
333
+ field_updates = {
334
+ "pr.number" => pr_data[:number].to_s,
335
+ "pr.url" => pr_data[:url].to_s
336
+ }
337
+ field_updates["pr.created_at"] = pr_data[:created_at].iso8601 if pr_data[:created_at]
338
+
339
+ result = task_manager.update(task_ref, set: field_updates)
340
+ !result.nil?
341
+ rescue
342
+ # Fall back to CLI on API error
343
+ add_pr_metadata_via_cli(task_ref, pr_data)
344
+ end
345
+
346
+ # Add PR metadata using CLI
347
+ #
348
+ # @param task_ref [String] Task reference
349
+ # @param pr_data [Hash] PR data
350
+ # @return [Boolean] true if successful
351
+ def add_pr_metadata_via_cli(task_ref, pr_data)
352
+ normalized_ref = normalize_task_reference(task_ref)
353
+ return false unless normalized_ref
354
+
355
+ success = true
356
+
357
+ result = execute_ace_task_command("update", normalized_ref, "--set", "pr.number=#{pr_data[:number]}")
358
+ success &&= result[:success]
359
+
360
+ result = execute_ace_task_command("update", normalized_ref, "--set", "pr.url=#{pr_data[:url]}")
361
+ success &&= result[:success]
362
+
363
+ if pr_data[:created_at]
364
+ timestamp = pr_data[:created_at].respond_to?(:iso8601) ? pr_data[:created_at].iso8601 : pr_data[:created_at].to_s
365
+ result = execute_ace_task_command("update", normalized_ref, "--set", "pr.created_at=#{timestamp}")
366
+ success &&= result[:success]
367
+ end
368
+
369
+ success
370
+ end
371
+
372
+ # Add started_at timestamp using Ruby API
373
+ #
374
+ # @param task_ref [String] Task reference
375
+ # @param started_at [Time] Start timestamp
376
+ # @return [Boolean] true if successful
377
+ def add_started_at_via_api(task_ref, started_at)
378
+ task_manager = Ace::Task::Organisms::TaskManager.new
379
+
380
+ field_updates = {"started_at" => started_at.iso8601}
381
+
382
+ result = task_manager.update(task_ref, set: field_updates)
383
+ !result.nil?
384
+ rescue
385
+ # Fall back to CLI on API error
386
+ add_started_at_via_cli(task_ref, started_at)
387
+ end
388
+
389
+ # Add started_at timestamp using CLI
390
+ #
391
+ # @param task_ref [String] Task reference
392
+ # @param started_at [Time] Start timestamp
393
+ # @return [Boolean] true if successful
394
+ def add_started_at_via_cli(task_ref, started_at)
395
+ normalized_ref = normalize_task_reference(task_ref)
396
+ return false unless normalized_ref
397
+
398
+ timestamp = started_at.respond_to?(:iso8601) ? started_at.iso8601 : started_at.to_s
399
+ result = execute_ace_task_command("update", normalized_ref, "--set", "started_at=#{timestamp}")
400
+ result[:success]
401
+ end
402
+
403
+ # Normalize task reference to a standard format
404
+ #
405
+ # @param task_ref [String] Input task reference
406
+ # @return [String, nil] Normalized reference or nil if invalid
407
+ def normalize_task_reference(task_ref)
408
+ Atoms::TaskIDExtractor.normalize(task_ref)
409
+ end
410
+
411
+ # Execute ace-task command
412
+ #
413
+ # @param args [Array<String>] Command arguments
414
+ # @return [Hash] Result with :success, :output, :error, :exit_code
415
+ def execute_ace_task_command(*args)
416
+ require "open3"
417
+
418
+ full_command = ["ace-task"] + args
419
+
420
+ stdout, stderr, status = Open3.capture3(*full_command, timeout: @timeout)
421
+
422
+ {
423
+ success: status.success?,
424
+ output: stdout.to_s,
425
+ error: stderr.to_s,
426
+ exit_code: status.exitstatus
427
+ }
428
+ rescue Open3::CommandTimeout
429
+ {
430
+ success: false,
431
+ output: "",
432
+ error: "ace-task command timed out after #{@timeout} seconds",
433
+ exit_code: 124
434
+ }
435
+ rescue => e
436
+ {
437
+ success: false,
438
+ output: "",
439
+ error: "ace-task command failed: #{e.message}",
440
+ exit_code: 1
441
+ }
442
+ end
443
+ end
444
+ end
445
+ end
446
+ end
447
+ end