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.
- checksums.yaml +7 -0
- data/.ace-defaults/git/worktree.yml +250 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +19 -0
- data/CHANGELOG.md +957 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +14 -0
- data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
- data/docs/demo/ace-git-worktree-getting-started.tape.yml +28 -0
- data/docs/demo/fixtures/README.md +3 -0
- data/docs/demo/fixtures/sample.txt +1 -0
- data/docs/getting-started.md +114 -0
- data/docs/handbook.md +38 -0
- data/docs/usage.md +334 -0
- data/exe/ace-git-worktree +24 -0
- data/handbook/agents/worktree.ag.md +189 -0
- data/handbook/skills/as-git-worktree/SKILL.md +27 -0
- data/handbook/skills/as-git-worktree-create/SKILL.md +21 -0
- data/handbook/skills/as-git-worktree-manage/SKILL.md +20 -0
- data/handbook/workflow-instructions/git/worktree-create.wf.md +262 -0
- data/handbook/workflow-instructions/git/worktree-manage.wf.md +384 -0
- data/handbook/workflow-instructions/git/worktree.wf.md +224 -0
- data/lib/ace/git/worktree/atoms/git_command.rb +121 -0
- data/lib/ace/git/worktree/atoms/path_expander.rb +189 -0
- data/lib/ace/git/worktree/atoms/slug_generator.rb +235 -0
- data/lib/ace/git/worktree/atoms/task_id_extractor.rb +91 -0
- data/lib/ace/git/worktree/cli/commands/config.rb +50 -0
- data/lib/ace/git/worktree/cli/commands/create.rb +80 -0
- data/lib/ace/git/worktree/cli/commands/list.rb +76 -0
- data/lib/ace/git/worktree/cli/commands/prune.rb +43 -0
- data/lib/ace/git/worktree/cli/commands/remove.rb +48 -0
- data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +66 -0
- data/lib/ace/git/worktree/cli/commands/switch.rb +44 -0
- data/lib/ace/git/worktree/cli.rb +103 -0
- data/lib/ace/git/worktree/commands/config_command.rb +351 -0
- data/lib/ace/git/worktree/commands/create_command.rb +961 -0
- data/lib/ace/git/worktree/commands/list_command.rb +247 -0
- data/lib/ace/git/worktree/commands/prune_command.rb +260 -0
- data/lib/ace/git/worktree/commands/remove_command.rb +522 -0
- data/lib/ace/git/worktree/commands/switch_command.rb +249 -0
- data/lib/ace/git/worktree/configuration.rb +167 -0
- data/lib/ace/git/worktree/models/worktree_config.rb +502 -0
- data/lib/ace/git/worktree/models/worktree_info.rb +303 -0
- data/lib/ace/git/worktree/models/worktree_metadata.rb +294 -0
- data/lib/ace/git/worktree/molecules/config_loader.rb +125 -0
- data/lib/ace/git/worktree/molecules/current_task_linker.rb +136 -0
- data/lib/ace/git/worktree/molecules/hook_executor.rb +361 -0
- data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +186 -0
- data/lib/ace/git/worktree/molecules/pr_creator.rb +253 -0
- data/lib/ace/git/worktree/molecules/task_committer.rb +329 -0
- data/lib/ace/git/worktree/molecules/task_fetcher.rb +244 -0
- data/lib/ace/git/worktree/molecules/task_pusher.rb +183 -0
- data/lib/ace/git/worktree/molecules/task_status_updater.rb +447 -0
- data/lib/ace/git/worktree/molecules/worktree_creator.rb +832 -0
- data/lib/ace/git/worktree/molecules/worktree_lister.rb +337 -0
- data/lib/ace/git/worktree/molecules/worktree_remover.rb +416 -0
- data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +906 -0
- data/lib/ace/git/worktree/organisms/worktree_manager.rb +714 -0
- data/lib/ace/git/worktree/version.rb +9 -0
- data/lib/ace/git/worktree.rb +215 -0
- metadata +218 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../atoms/task_id_extractor"
|
|
4
|
+
require_relative "../molecules/current_task_linker"
|
|
5
|
+
require_relative "../molecules/parent_task_resolver"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Git
|
|
9
|
+
module Worktree
|
|
10
|
+
module Organisms
|
|
11
|
+
# Task worktree orchestrator
|
|
12
|
+
#
|
|
13
|
+
# Orchestrates the complete workflow of creating task-aware worktrees,
|
|
14
|
+
# including task status updates, metadata tracking, commits, and mise trust.
|
|
15
|
+
# This is the main high-level interface for task-worktree integration.
|
|
16
|
+
#
|
|
17
|
+
# @example Create a complete task worktree workflow
|
|
18
|
+
# orchestrator = TaskWorktreeOrchestrator.new
|
|
19
|
+
# result = orchestrator.create_for_task("081")
|
|
20
|
+
# # => { success: true, worktree_path: "/project/.ace-wt/task.081", ... }
|
|
21
|
+
class TaskWorktreeOrchestrator
|
|
22
|
+
# Initialize a new TaskWorktreeOrchestrator
|
|
23
|
+
#
|
|
24
|
+
# @param config [WorktreeConfig, nil] Worktree configuration (loaded if nil)
|
|
25
|
+
# @param project_root [String] Project root directory
|
|
26
|
+
def initialize(config: nil, project_root: Dir.pwd)
|
|
27
|
+
@project_root = project_root
|
|
28
|
+
@config = config || load_configuration
|
|
29
|
+
@task_fetcher = Molecules::TaskFetcher.new
|
|
30
|
+
@task_status_updater = Molecules::TaskStatusUpdater.new
|
|
31
|
+
@task_committer = Molecules::TaskCommitter.new
|
|
32
|
+
@task_pusher = Molecules::TaskPusher.new
|
|
33
|
+
@worktree_creator = Molecules::WorktreeCreator.new
|
|
34
|
+
@pr_creator = Molecules::PrCreator.new
|
|
35
|
+
@parent_task_resolver = Molecules::ParentTaskResolver.new(project_root: project_root)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Create a worktree for a task with complete workflow
|
|
39
|
+
#
|
|
40
|
+
# @param task_ref [String] Task reference (081, task.081, v.0.9.0+081)
|
|
41
|
+
# @param options [Hash] Options for worktree creation
|
|
42
|
+
# @option options [String] :source Git ref to use as branch start-point (default: current branch)
|
|
43
|
+
# @return [Hash] Result with workflow details
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# orchestrator = TaskWorktreeOrchestrator.new
|
|
47
|
+
# result = orchestrator.create_for_task("081")
|
|
48
|
+
# # => {
|
|
49
|
+
# # success: true,
|
|
50
|
+
# # worktree_path: "/project/.ace-wt/task.081",
|
|
51
|
+
# # branch: "081-fix-authentication-bug",
|
|
52
|
+
# # task_id: "081",
|
|
53
|
+
# # steps_completed: ["task_fetched", "status_updated", "worktree_created", "mise_trusted"],
|
|
54
|
+
# # error: nil
|
|
55
|
+
# # }
|
|
56
|
+
#
|
|
57
|
+
# @example With explicit source
|
|
58
|
+
# result = orchestrator.create_for_task("081", source: "main")
|
|
59
|
+
# # => Creates branch based on 'main' instead of current branch
|
|
60
|
+
def create_for_task(task_ref, options = {})
|
|
61
|
+
workflow_result = initialize_workflow_result
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
# Step 1: Fetch task data
|
|
65
|
+
task_data = fetch_task_data(task_ref)
|
|
66
|
+
return error_workflow_result("Task not found: #{task_ref}", workflow_result) unless task_data
|
|
67
|
+
|
|
68
|
+
task_id = extract_task_id(task_data)
|
|
69
|
+
workflow_result[:task_id] = task_id
|
|
70
|
+
workflow_result[:task_title] = task_data[:title]
|
|
71
|
+
workflow_result[:steps_completed] << "task_fetched"
|
|
72
|
+
|
|
73
|
+
# Step 2: Check if worktree already exists
|
|
74
|
+
existing_worktree = check_existing_worktree(task_data)
|
|
75
|
+
if existing_worktree
|
|
76
|
+
return success_workflow_result("Worktree already exists", workflow_result.merge(
|
|
77
|
+
worktree_path: existing_worktree.path,
|
|
78
|
+
branch: existing_worktree.branch,
|
|
79
|
+
existing: true
|
|
80
|
+
))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Step 3: Update task status if configured (and not overridden)
|
|
84
|
+
should_update_status = options[:no_status_update] ? false : @config.auto_mark_in_progress?
|
|
85
|
+
if should_update_status && task_data[:status] != "in-progress"
|
|
86
|
+
status_result = update_task_status(task_id, "in-progress")
|
|
87
|
+
if status_result[:success]
|
|
88
|
+
workflow_result[:steps_completed] << "status_updated"
|
|
89
|
+
else
|
|
90
|
+
error_message = status_result[:message] || "Failed to update task status"
|
|
91
|
+
hint = "\n\nHint: Use --no-status-update to create worktree without changing task status"
|
|
92
|
+
return error_workflow_result(error_message + hint, workflow_result)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Step 4: Create worktree metadata
|
|
97
|
+
# Determine target branch for PR (parent's branch for subtasks, or main)
|
|
98
|
+
target_branch = resolve_target_branch(task_data, options)
|
|
99
|
+
workflow_result[:target_branch] = target_branch
|
|
100
|
+
worktree_metadata = create_worktree_metadata(task_data, target_branch: target_branch)
|
|
101
|
+
workflow_result[:steps_completed] << "metadata_prepared"
|
|
102
|
+
|
|
103
|
+
# Step 5: Add worktree metadata to task file (BEFORE commit so it's included)
|
|
104
|
+
if @config.add_worktree_metadata?
|
|
105
|
+
if add_worktree_metadata_to_task(task_data, worktree_metadata)
|
|
106
|
+
workflow_result[:steps_completed] << "metadata_added"
|
|
107
|
+
else
|
|
108
|
+
workflow_result[:warnings] << "Failed to add worktree metadata to task"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Step 6: Commit task changes if configured (includes status + metadata)
|
|
113
|
+
# Commit when either status was updated or metadata was added
|
|
114
|
+
should_commit = options[:no_commit] ? false : @config.auto_commit_task?
|
|
115
|
+
metadata_was_added = workflow_result[:steps_completed].include?("metadata_added")
|
|
116
|
+
has_changes_to_commit = should_update_status || metadata_was_added
|
|
117
|
+
if should_commit && has_changes_to_commit
|
|
118
|
+
commit_message = options[:commit_message] || "in-progress"
|
|
119
|
+
if commit_task_changes(task_data, commit_message)
|
|
120
|
+
workflow_result[:steps_completed] << "task_committed"
|
|
121
|
+
else
|
|
122
|
+
# Continue even if commit fails, but note it
|
|
123
|
+
workflow_result[:warnings] << "Failed to commit task changes"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Step 7: Push task changes if configured (so PR shows updates)
|
|
128
|
+
should_push = options[:no_push] ? false : @config.auto_push_task?
|
|
129
|
+
if should_push && should_commit && workflow_result[:steps_completed].include?("task_committed")
|
|
130
|
+
push_remote = options[:push_remote] || @config.push_remote
|
|
131
|
+
if push_task_changes(push_remote)
|
|
132
|
+
workflow_result[:steps_completed] << "task_pushed"
|
|
133
|
+
workflow_result[:pushed_to] = push_remote
|
|
134
|
+
else
|
|
135
|
+
# Continue even if push fails, but note it
|
|
136
|
+
workflow_result[:warnings] << "Failed to push task changes"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Step 8: Create the worktree
|
|
141
|
+
worktree_result = create_worktree_for_task(task_data, worktree_metadata, source: options[:source])
|
|
142
|
+
return error_workflow_result(worktree_result[:error], workflow_result) unless worktree_result[:success]
|
|
143
|
+
|
|
144
|
+
workflow_result[:worktree_path] = worktree_result[:worktree_path]
|
|
145
|
+
workflow_result[:branch] = worktree_result[:branch]
|
|
146
|
+
workflow_result[:directory_name] = worktree_result[:directory_name]
|
|
147
|
+
workflow_result[:start_point] = worktree_result[:start_point]
|
|
148
|
+
workflow_result[:steps_completed] << "worktree_created"
|
|
149
|
+
|
|
150
|
+
# Step 8.5: Create _current symlink in worktree if configured
|
|
151
|
+
if @config.create_current_symlink?
|
|
152
|
+
current_linker = Molecules::CurrentTaskLinker.new(
|
|
153
|
+
project_root: worktree_result[:worktree_path],
|
|
154
|
+
symlink_name: @config.current_symlink_name
|
|
155
|
+
)
|
|
156
|
+
# Task directory relative to worktree (same structure as main repo)
|
|
157
|
+
# task_data[:path] is the task file path, we need the parent directory
|
|
158
|
+
worktree_task_dir = File.dirname(File.join(worktree_result[:worktree_path], relative_task_path(task_data[:path])))
|
|
159
|
+
link_result = current_linker.link(worktree_task_dir)
|
|
160
|
+
if link_result[:success]
|
|
161
|
+
workflow_result[:steps_completed] << "current_symlink_created"
|
|
162
|
+
workflow_result[:current_symlink] = link_result[:symlink_path]
|
|
163
|
+
else
|
|
164
|
+
# Symlink creation is non-blocking - failure becomes warning
|
|
165
|
+
workflow_result[:warnings] ||= []
|
|
166
|
+
workflow_result[:warnings] << "Failed to create _current symlink: #{link_result[:error]}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Step 9: Setup upstream for worktree branch if configured
|
|
171
|
+
should_setup_upstream = @config.auto_setup_upstream? && !options[:no_upstream]
|
|
172
|
+
if should_setup_upstream
|
|
173
|
+
upstream_result = setup_upstream_for_worktree(worktree_result, options)
|
|
174
|
+
if upstream_result[:success]
|
|
175
|
+
workflow_result[:steps_completed] << "upstream_setup"
|
|
176
|
+
workflow_result[:pushed_branch] = upstream_result[:branch]
|
|
177
|
+
else
|
|
178
|
+
# Upstream setup is non-blocking - failure becomes warning
|
|
179
|
+
workflow_result[:warnings] ||= []
|
|
180
|
+
workflow_result[:warnings] << "Failed to setup upstream: #{upstream_result[:error]}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Step 9.5: Add started_at timestamp to task IN WORKTREE (creates initial commit for PR)
|
|
185
|
+
# Only do this if we're going to create a PR and upstream succeeded
|
|
186
|
+
upstream_succeeded = workflow_result[:steps_completed].include?("upstream_setup")
|
|
187
|
+
should_create_pr = @config.auto_create_pr? && !options[:no_pr]
|
|
188
|
+
if should_create_pr && upstream_succeeded
|
|
189
|
+
started_result = add_started_timestamp_in_worktree(task_data, worktree_result, options)
|
|
190
|
+
if started_result[:success]
|
|
191
|
+
workflow_result[:steps_completed] << "started_at_added"
|
|
192
|
+
else
|
|
193
|
+
# Non-blocking - PR creation may still work if branch already has commits
|
|
194
|
+
workflow_result[:warnings] ||= []
|
|
195
|
+
workflow_result[:warnings] << "Failed to add started_at: #{started_result[:error]}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Step 10: Create draft PR if configured
|
|
200
|
+
if should_create_pr && upstream_succeeded
|
|
201
|
+
pr_result = create_pr_for_task(task_data, worktree_result, options)
|
|
202
|
+
if pr_result[:success]
|
|
203
|
+
workflow_result[:steps_completed] << "pr_created"
|
|
204
|
+
workflow_result[:pr_number] = pr_result[:pr_number]
|
|
205
|
+
workflow_result[:pr_url] = pr_result[:pr_url]
|
|
206
|
+
workflow_result[:pr_existing] = pr_result[:existing]
|
|
207
|
+
|
|
208
|
+
# Step 11: Save PR metadata to task
|
|
209
|
+
save_pr_result = save_pr_to_task(task_data, pr_result)
|
|
210
|
+
if save_pr_result
|
|
211
|
+
workflow_result[:steps_completed] << "pr_saved_to_task"
|
|
212
|
+
else
|
|
213
|
+
workflow_result[:warnings] ||= []
|
|
214
|
+
workflow_result[:warnings] << "Failed to save PR metadata to task"
|
|
215
|
+
end
|
|
216
|
+
else
|
|
217
|
+
# PR creation is non-blocking - failure becomes warning
|
|
218
|
+
workflow_result[:warnings] ||= []
|
|
219
|
+
workflow_result[:warnings] << "Failed to create PR: #{pr_result[:error]}"
|
|
220
|
+
end
|
|
221
|
+
elsif should_create_pr && !upstream_succeeded
|
|
222
|
+
# Skip PR creation if upstream setup failed
|
|
223
|
+
workflow_result[:warnings] ||= []
|
|
224
|
+
workflow_result[:warnings] << "Skipped PR creation: branch not pushed to remote"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Step 12: Run after-create hooks if configured
|
|
228
|
+
hooks = @config.after_create_hooks
|
|
229
|
+
if hooks && hooks.any?
|
|
230
|
+
require_relative "../molecules/hook_executor"
|
|
231
|
+
hook_executor = Molecules::HookExecutor.new
|
|
232
|
+
hook_result = hook_executor.execute_hooks(
|
|
233
|
+
hooks,
|
|
234
|
+
worktree_path: worktree_result[:worktree_path],
|
|
235
|
+
project_root: @project_root,
|
|
236
|
+
task_data: task_data
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if hook_result[:success]
|
|
240
|
+
workflow_result[:steps_completed] << "hooks_executed"
|
|
241
|
+
workflow_result[:hooks_results] = hook_result[:results]
|
|
242
|
+
else
|
|
243
|
+
# Hooks are non-blocking - failures become warnings
|
|
244
|
+
workflow_result[:warnings] ||= []
|
|
245
|
+
workflow_result[:warnings] += hook_result[:errors]
|
|
246
|
+
workflow_result[:hooks_results] = hook_result[:results]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Success!
|
|
251
|
+
success_workflow_result("Task worktree created successfully", workflow_result)
|
|
252
|
+
rescue => e
|
|
253
|
+
error_workflow_result("Unexpected error: #{e.message}", workflow_result)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Create a worktree with dry run (no actual changes)
|
|
258
|
+
#
|
|
259
|
+
# @param task_ref [String] Task reference
|
|
260
|
+
# @param options [Hash] Options for dry run
|
|
261
|
+
# @return [Hash] Dry run result showing what would be done
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# result = orchestrator.dry_run_create("081")
|
|
265
|
+
# # => { success: true, would_create: {...}, steps: [...] }
|
|
266
|
+
def dry_run_create(task_ref, options = {})
|
|
267
|
+
workflow_result = initialize_workflow_result
|
|
268
|
+
|
|
269
|
+
begin
|
|
270
|
+
# Step 1: Fetch task data
|
|
271
|
+
task_data = fetch_task_data(task_ref)
|
|
272
|
+
return error_workflow_result("Task not found: #{task_ref}", workflow_result) unless task_data
|
|
273
|
+
|
|
274
|
+
task_id = extract_task_id(task_data)
|
|
275
|
+
workflow_result[:task_id] = task_id
|
|
276
|
+
workflow_result[:task_title] = task_data[:title]
|
|
277
|
+
workflow_result[:steps_completed] << "task_fetched"
|
|
278
|
+
|
|
279
|
+
# Step 2: Check what would be created
|
|
280
|
+
directory_name = @config.format_directory(task_data)
|
|
281
|
+
branch_name = @config.format_branch(task_data)
|
|
282
|
+
worktree_path = File.join(@config.absolute_root_path, directory_name)
|
|
283
|
+
options[:source] || "main"
|
|
284
|
+
|
|
285
|
+
# Determine target branch for PR (parent's branch for subtasks, or main)
|
|
286
|
+
target_branch = resolve_target_branch(task_data, options)
|
|
287
|
+
|
|
288
|
+
# Determine upstream/PR settings (considering options)
|
|
289
|
+
should_setup_upstream = @config.auto_setup_upstream? && !options[:no_upstream]
|
|
290
|
+
should_create_pr = @config.auto_create_pr? && !options[:no_pr] && should_setup_upstream
|
|
291
|
+
|
|
292
|
+
# Determine if there will be changes to commit
|
|
293
|
+
# Commit when either status would be updated or metadata would be added
|
|
294
|
+
would_update_status = @config.auto_mark_in_progress? && task_data[:status] != "in-progress"
|
|
295
|
+
would_add_metadata = @config.add_worktree_metadata?
|
|
296
|
+
has_changes_to_commit = would_update_status || would_add_metadata
|
|
297
|
+
would_commit = @config.auto_commit_task? && has_changes_to_commit
|
|
298
|
+
|
|
299
|
+
# Determine if current symlink would be created (in worktree)
|
|
300
|
+
would_create_current_symlink = @config.create_current_symlink?
|
|
301
|
+
current_symlink_path = would_create_current_symlink ? File.join(worktree_path, @config.current_symlink_name) : nil
|
|
302
|
+
# Task directory relative to worktree (same structure as main repo)
|
|
303
|
+
# task_data[:path] is the task file path, we need the parent directory
|
|
304
|
+
relative_task = would_create_current_symlink ? relative_task_path(task_data[:path]) : nil
|
|
305
|
+
worktree_task_dir = would_create_current_symlink ? File.dirname(File.join(worktree_path, relative_task)) : nil
|
|
306
|
+
|
|
307
|
+
workflow_result[:would_create] = {
|
|
308
|
+
worktree_path: worktree_path,
|
|
309
|
+
branch: branch_name,
|
|
310
|
+
directory_name: directory_name,
|
|
311
|
+
target_branch: target_branch,
|
|
312
|
+
task_status_update: would_update_status,
|
|
313
|
+
metadata_addition: would_add_metadata,
|
|
314
|
+
task_commit: would_commit,
|
|
315
|
+
task_push: @config.auto_push_task? && would_commit,
|
|
316
|
+
push_remote: @config.push_remote,
|
|
317
|
+
current_symlink: would_create_current_symlink,
|
|
318
|
+
current_symlink_path: current_symlink_path,
|
|
319
|
+
current_symlink_target: worktree_task_dir,
|
|
320
|
+
upstream_push: should_setup_upstream,
|
|
321
|
+
add_started_at: should_create_pr && should_setup_upstream,
|
|
322
|
+
create_pr: should_create_pr,
|
|
323
|
+
pr_title: should_create_pr ? @config.format_pr_title(task_data) : nil,
|
|
324
|
+
pr_base: should_create_pr ? target_branch : nil,
|
|
325
|
+
hooks_count: @config.after_create_hooks.length
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
workflow_result[:steps_planned] = [
|
|
329
|
+
"fetch_task_data",
|
|
330
|
+
("update_task_status" if workflow_result[:would_create][:task_status_update]),
|
|
331
|
+
("add_worktree_metadata" if workflow_result[:would_create][:metadata_addition]),
|
|
332
|
+
("commit_task_changes" if workflow_result[:would_create][:task_commit]),
|
|
333
|
+
("push_to_#{workflow_result[:would_create][:push_remote]}" if workflow_result[:would_create][:task_push]),
|
|
334
|
+
"create_worktree",
|
|
335
|
+
("create_current_symlink" if workflow_result[:would_create][:current_symlink]),
|
|
336
|
+
("setup_upstream_tracking" if should_setup_upstream),
|
|
337
|
+
("add_started_at_in_worktree" if workflow_result[:would_create][:add_started_at]),
|
|
338
|
+
("create_draft_pr" if should_create_pr),
|
|
339
|
+
("save_pr_metadata" if should_create_pr),
|
|
340
|
+
("execute_#{workflow_result[:would_create][:hooks_count]}_hooks" if workflow_result[:would_create][:hooks_count] > 0)
|
|
341
|
+
].compact
|
|
342
|
+
|
|
343
|
+
success_workflow_result("Dry run completed", workflow_result)
|
|
344
|
+
rescue => e
|
|
345
|
+
error_workflow_result("Dry run error: #{e.message}", workflow_result)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Remove a task worktree with cleanup
|
|
350
|
+
#
|
|
351
|
+
# @param task_ref [String] Task reference
|
|
352
|
+
# @param options [Hash] Options for removal
|
|
353
|
+
# @return [Hash] Result of removal workflow
|
|
354
|
+
#
|
|
355
|
+
# @example
|
|
356
|
+
# result = orchestrator.remove_task_worktree("081", force: true)
|
|
357
|
+
def remove_task_worktree(task_ref, options = {})
|
|
358
|
+
workflow_result = initialize_workflow_result
|
|
359
|
+
|
|
360
|
+
begin
|
|
361
|
+
# Step 1: Fetch task data
|
|
362
|
+
task_data = fetch_task_data(task_ref)
|
|
363
|
+
|
|
364
|
+
# Step 2: Find existing worktree (with fallback for missing task)
|
|
365
|
+
if task_data
|
|
366
|
+
task_id = extract_task_id(task_data)
|
|
367
|
+
workflow_result[:task_id] = task_id
|
|
368
|
+
workflow_result[:steps_completed] << "task_fetched"
|
|
369
|
+
worktree_info = find_worktree_for_task_data(task_data)
|
|
370
|
+
else
|
|
371
|
+
# Fallback: Try to find worktree by task reference even if task data not found
|
|
372
|
+
worktree_info = find_worktree_by_task_reference(task_ref)
|
|
373
|
+
if worktree_info
|
|
374
|
+
puts "Task not found, but worktree found. Removing worktree without updating task data."
|
|
375
|
+
workflow_result[:task_id] = task_ref
|
|
376
|
+
workflow_result[:task_not_found] = true
|
|
377
|
+
else
|
|
378
|
+
return error_workflow_result("Task not found: #{task_ref}", workflow_result)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
return error_workflow_result("No worktree found for task", workflow_result) unless worktree_info
|
|
383
|
+
|
|
384
|
+
workflow_result[:worktree_path] = worktree_info.path
|
|
385
|
+
workflow_result[:branch] = worktree_info.branch
|
|
386
|
+
|
|
387
|
+
# Step 3: Check removal safety
|
|
388
|
+
worktree_remover = Molecules::WorktreeRemover.new
|
|
389
|
+
safety_check = worktree_remover.check_removal_safety(worktree_info.path)
|
|
390
|
+
unless options[:force] || safety_check[:safe]
|
|
391
|
+
return error_workflow_result("Cannot remove worktree: #{safety_check[:errors].join(", ")}", workflow_result)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Step 4: Remove worktree metadata from task (only if task was found)
|
|
395
|
+
if task_data && remove_worktree_metadata_from_task(task_data)
|
|
396
|
+
workflow_result[:steps_completed] << "metadata_removed"
|
|
397
|
+
elsif workflow_result[:task_not_found]
|
|
398
|
+
workflow_result[:steps_completed] << "skipped_metadata_cleanup"
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Step 5: Remove the worktree
|
|
402
|
+
remove_result = worktree_remover.remove(worktree_info.path, force: options[:force])
|
|
403
|
+
return error_workflow_result("Failed to remove worktree: #{remove_result[:error]}", workflow_result) unless remove_result[:success]
|
|
404
|
+
|
|
405
|
+
workflow_result[:steps_completed] << "worktree_removed"
|
|
406
|
+
|
|
407
|
+
success_workflow_result("Task worktree removed successfully", workflow_result)
|
|
408
|
+
rescue => e
|
|
409
|
+
error_workflow_result("Unexpected error: #{e.message}", workflow_result)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Get status of task worktrees
|
|
414
|
+
#
|
|
415
|
+
# @param task_refs [Array<String>, nil] Task references to check (all if nil)
|
|
416
|
+
# @return [Hash] Status information
|
|
417
|
+
#
|
|
418
|
+
# @example
|
|
419
|
+
# status = orchestrator.get_task_worktree_status(["081", "082"])
|
|
420
|
+
def get_task_worktree_status(task_refs = nil)
|
|
421
|
+
if task_refs.nil?
|
|
422
|
+
# Get all task-associated worktrees
|
|
423
|
+
worktree_lister = Molecules::WorktreeLister.new
|
|
424
|
+
worktrees = worktree_lister.list_all.select(&:task_associated?)
|
|
425
|
+
task_ids = worktrees.map(&:task_id).compact.uniq
|
|
426
|
+
else
|
|
427
|
+
task_ids = Array(task_refs).map { |ref| Atoms::TaskIDExtractor.normalize(ref) }.compact
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
status_info = {
|
|
431
|
+
total_tasks: task_ids.length,
|
|
432
|
+
worktrees: []
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
task_ids.each do |task_id|
|
|
436
|
+
worktree_info = @worktree_creator.find_by_task_id(task_id)
|
|
437
|
+
task_metadata = @task_fetcher.fetch(task_id)
|
|
438
|
+
|
|
439
|
+
worktree_status = {
|
|
440
|
+
task_id: task_id,
|
|
441
|
+
task_title: task_metadata&.title,
|
|
442
|
+
task_status: task_metadata&.status,
|
|
443
|
+
has_worktree: !worktree_info.nil?,
|
|
444
|
+
worktree_path: worktree_info&.path,
|
|
445
|
+
worktree_branch: worktree_info&.branch,
|
|
446
|
+
worktree_exists: worktree_info&.exists?,
|
|
447
|
+
worktree_usable: worktree_info&.usable?
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
status_info[:worktrees] << worktree_status
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
status_info[:worktrees_with_worktrees] = status_info[:worktrees].count { |w| w[:has_worktree] }
|
|
454
|
+
status_info[:active_worktrees] = status_info[:worktrees].count { |w| w[:worktree_exists] && w[:worktree_usable] }
|
|
455
|
+
|
|
456
|
+
{success: true, status: status_info}
|
|
457
|
+
rescue => e
|
|
458
|
+
error_result("Failed to get task worktree status: #{e.message}")
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
private
|
|
462
|
+
|
|
463
|
+
# Find worktree by task reference (fallback when task metadata not found)
|
|
464
|
+
#
|
|
465
|
+
# @param task_ref [String] Task reference (e.g., "090", "task.090")
|
|
466
|
+
# @return [WorktreeInfo, nil] Worktree info or nil if not found
|
|
467
|
+
def find_worktree_by_task_reference(task_ref)
|
|
468
|
+
# Get all worktrees using WorktreeLister
|
|
469
|
+
worktree_lister = Molecules::WorktreeLister.new
|
|
470
|
+
worktrees = worktree_lister.list_all
|
|
471
|
+
|
|
472
|
+
# Normalize task reference to match worktree IDs
|
|
473
|
+
normalized_id = normalize_task_id_for_matching(task_ref)
|
|
474
|
+
|
|
475
|
+
# Find worktree with matching task ID
|
|
476
|
+
worktrees.find do |worktree|
|
|
477
|
+
worktree.task_id == normalized_id
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Normalize task ID for worktree matching
|
|
482
|
+
#
|
|
483
|
+
# @param task_ref [String] Task reference (e.g., "090", "121.01", "task.121.01")
|
|
484
|
+
# @return [String] Normalized task ID (preserves subtask suffix)
|
|
485
|
+
def normalize_task_id_for_matching(task_ref)
|
|
486
|
+
# Use shared extractor that preserves subtask IDs (e.g., "121.01")
|
|
487
|
+
Atoms::TaskIDExtractor.normalize(task_ref) || task_ref
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Initialize workflow result structure
|
|
491
|
+
#
|
|
492
|
+
# @return [Hash] Initial workflow result
|
|
493
|
+
def initialize_workflow_result
|
|
494
|
+
{
|
|
495
|
+
success: false,
|
|
496
|
+
task_id: nil,
|
|
497
|
+
task_title: nil,
|
|
498
|
+
worktree_path: nil,
|
|
499
|
+
branch: nil,
|
|
500
|
+
directory_name: nil,
|
|
501
|
+
steps_completed: [],
|
|
502
|
+
steps_planned: [],
|
|
503
|
+
warnings: [],
|
|
504
|
+
error: nil,
|
|
505
|
+
existing: false
|
|
506
|
+
}
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Load configuration
|
|
510
|
+
#
|
|
511
|
+
# @return [WorktreeConfig] Loaded configuration
|
|
512
|
+
def load_configuration
|
|
513
|
+
loader = Molecules::ConfigLoader.new(@project_root)
|
|
514
|
+
loader.load
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Fetch task data
|
|
518
|
+
#
|
|
519
|
+
# @param task_ref [String] Task reference
|
|
520
|
+
# @return [Hash, nil] Task data hash or nil
|
|
521
|
+
def fetch_task_data(task_ref)
|
|
522
|
+
@task_fetcher.fetch(task_ref)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Resolve target branch for PR
|
|
526
|
+
#
|
|
527
|
+
# Uses CLI-provided target_branch if present, otherwise auto-detects from parent task.
|
|
528
|
+
# For subtasks, returns parent's worktree branch. For orchestrators, returns "main".
|
|
529
|
+
#
|
|
530
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
531
|
+
# @param options [Hash] Options hash (may contain :target_branch)
|
|
532
|
+
# @return [String] Target branch name
|
|
533
|
+
def resolve_target_branch(task_data, options)
|
|
534
|
+
options[:target_branch] || @parent_task_resolver.resolve_target_branch(task_data)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Check if worktree already exists for task
|
|
538
|
+
#
|
|
539
|
+
# @param task_data [Hash] Task data hash
|
|
540
|
+
# @return [WorktreeInfo, nil] Existing worktree or nil
|
|
541
|
+
def check_existing_worktree(task_data)
|
|
542
|
+
@worktree_creator.worktree_exists?(task_data: task_data)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Update task status
|
|
546
|
+
#
|
|
547
|
+
# @param task_id [String] Task ID
|
|
548
|
+
# @param status [String] New status
|
|
549
|
+
# @return [Hash] Result with :success and :message keys
|
|
550
|
+
def update_task_status(task_id, status)
|
|
551
|
+
@task_status_updater.update_status(task_id, status)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# Create worktree metadata
|
|
555
|
+
#
|
|
556
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
557
|
+
# @param target_branch [String, nil] PR target branch (for subtasks)
|
|
558
|
+
# @return [WorktreeMetadata] Worktree metadata
|
|
559
|
+
def create_worktree_metadata(task_data, target_branch: nil)
|
|
560
|
+
# Generate worktree path and branch names
|
|
561
|
+
directory_name = @config.format_directory(task_data)
|
|
562
|
+
branch_name = @config.format_branch(task_data)
|
|
563
|
+
File.join(@config.absolute_root_path, directory_name)
|
|
564
|
+
|
|
565
|
+
Models::WorktreeMetadata.new(
|
|
566
|
+
branch: branch_name,
|
|
567
|
+
path: File.join(@config.root_path, directory_name),
|
|
568
|
+
target_branch: target_branch,
|
|
569
|
+
created_at: Time.now
|
|
570
|
+
)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Commit task changes
|
|
574
|
+
#
|
|
575
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
576
|
+
# @param status [String] Task status
|
|
577
|
+
# @return [Boolean] true if successful
|
|
578
|
+
def commit_task_changes(task_data, status)
|
|
579
|
+
# Find the task file (this would need implementation)
|
|
580
|
+
# For now, commit all changes
|
|
581
|
+
task_id = extract_task_id(task_data)
|
|
582
|
+
@task_committer.commit_all_changes(status, task_id)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Push task changes to remote
|
|
586
|
+
#
|
|
587
|
+
# @param remote [String] Remote name (default: "origin")
|
|
588
|
+
# @return [Boolean] true if successful
|
|
589
|
+
def push_task_changes(remote = "origin")
|
|
590
|
+
result = @task_pusher.push(remote: remote)
|
|
591
|
+
result[:success]
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Create worktree for task
|
|
595
|
+
#
|
|
596
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
597
|
+
# @param worktree_metadata [WorktreeMetadata] Worktree metadata
|
|
598
|
+
# @param source [String, nil] Git ref to use as branch start-point
|
|
599
|
+
# @return [Hash] Worktree creation result
|
|
600
|
+
def create_worktree_for_task(task_data, worktree_metadata, source: nil)
|
|
601
|
+
@worktree_creator.create_for_task(
|
|
602
|
+
task_data,
|
|
603
|
+
@config,
|
|
604
|
+
source: source,
|
|
605
|
+
target_branch: worktree_metadata.target_branch
|
|
606
|
+
)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Add worktree metadata to task
|
|
610
|
+
#
|
|
611
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
612
|
+
# @param worktree_metadata [WorktreeMetadata] Worktree metadata
|
|
613
|
+
# @return [Boolean] true if successful
|
|
614
|
+
def add_worktree_metadata_to_task(task_data, worktree_metadata)
|
|
615
|
+
# Try to use ace-task update command first
|
|
616
|
+
task_id = extract_task_id(task_data)
|
|
617
|
+
if @task_status_updater.add_worktree_metadata(task_id, worktree_metadata)
|
|
618
|
+
return true
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# Fallback to direct file manipulation
|
|
622
|
+
# This would need implementation to find and update the task file
|
|
623
|
+
false
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Find worktree for task
|
|
627
|
+
#
|
|
628
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
629
|
+
# @return [WorktreeInfo, nil] Worktree info or nil
|
|
630
|
+
def find_worktree_for_task_data(task_data)
|
|
631
|
+
task_id = extract_task_id(task_data)
|
|
632
|
+
@worktree_creator.find_by_task_id(task_id)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Remove worktree metadata from task
|
|
636
|
+
#
|
|
637
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
638
|
+
# @return [Boolean] true if successful
|
|
639
|
+
def remove_worktree_metadata_from_task(task_data)
|
|
640
|
+
# This would need implementation to find and update the task file
|
|
641
|
+
false
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Setup upstream tracking for worktree branch
|
|
645
|
+
#
|
|
646
|
+
# Pushes the new branch to remote with -u flag to setup upstream tracking.
|
|
647
|
+
# If push fails but remote branch exists, falls back to git branch --set-upstream-to.
|
|
648
|
+
# Uses the worktree path to run git commands from within the worktree.
|
|
649
|
+
#
|
|
650
|
+
# @param worktree_result [Hash] Worktree creation result with :worktree_path, :branch
|
|
651
|
+
# @param options [Hash] Options hash (may include :push_remote)
|
|
652
|
+
# @return [Hash] Result with :success, :branch, :remote, :error, :method
|
|
653
|
+
def setup_upstream_for_worktree(worktree_result, options)
|
|
654
|
+
worktree_path = worktree_result[:worktree_path]
|
|
655
|
+
branch = worktree_result[:branch]
|
|
656
|
+
remote = options[:push_remote] || @config.push_remote || "origin"
|
|
657
|
+
|
|
658
|
+
begin
|
|
659
|
+
Dir.chdir(worktree_path) do
|
|
660
|
+
# Try push with -u first
|
|
661
|
+
result = @task_pusher.push(remote: remote, set_upstream: true)
|
|
662
|
+
|
|
663
|
+
if result[:success]
|
|
664
|
+
return {
|
|
665
|
+
success: true,
|
|
666
|
+
branch: branch,
|
|
667
|
+
remote: remote,
|
|
668
|
+
error: nil,
|
|
669
|
+
method: :push
|
|
670
|
+
}
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Push failed - check if remote branch exists and set upstream directly
|
|
674
|
+
if remote_branch_exists?(remote, branch)
|
|
675
|
+
upstream_result = @task_pusher.set_upstream(branch: branch, remote: remote)
|
|
676
|
+
if upstream_result[:success]
|
|
677
|
+
return {
|
|
678
|
+
success: true,
|
|
679
|
+
branch: branch,
|
|
680
|
+
remote: remote,
|
|
681
|
+
error: nil,
|
|
682
|
+
method: :set_upstream
|
|
683
|
+
}
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Both methods failed
|
|
688
|
+
{
|
|
689
|
+
success: false,
|
|
690
|
+
branch: branch,
|
|
691
|
+
remote: remote,
|
|
692
|
+
error: result[:error] || "Failed to setup upstream"
|
|
693
|
+
}
|
|
694
|
+
end
|
|
695
|
+
rescue => e
|
|
696
|
+
{
|
|
697
|
+
success: false,
|
|
698
|
+
branch: branch,
|
|
699
|
+
remote: remote,
|
|
700
|
+
error: e.message
|
|
701
|
+
}
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Check if a branch exists on the remote
|
|
706
|
+
#
|
|
707
|
+
# @param remote [String] Remote name (e.g., "origin")
|
|
708
|
+
# @param branch [String] Branch name to check
|
|
709
|
+
# @return [Boolean] true if remote branch exists
|
|
710
|
+
def remote_branch_exists?(remote, branch)
|
|
711
|
+
result = Atoms::GitCommand.execute("ls-remote", "--heads", remote, branch, timeout: 10)
|
|
712
|
+
result[:success] && result[:output]&.include?(branch)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Create draft PR for task
|
|
716
|
+
#
|
|
717
|
+
# Creates a draft PR targeting the source branch (start_point) from which
|
|
718
|
+
# the worktree branch was created.
|
|
719
|
+
#
|
|
720
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
721
|
+
# @param worktree_result [Hash] Worktree creation result with :branch, :start_point
|
|
722
|
+
# @param options [Hash] Options (may include :source for base branch override)
|
|
723
|
+
# @return [Hash] Result with :success, :pr_number, :pr_url, :existing, :error
|
|
724
|
+
def create_pr_for_task(task_data, worktree_result, options)
|
|
725
|
+
branch = worktree_result[:branch]
|
|
726
|
+
# Use target_branch from metadata if available (for subtasks)
|
|
727
|
+
# Otherwise fall back to source option or start_point
|
|
728
|
+
start_point = worktree_result[:target_branch] || options[:source] || worktree_result[:start_point]
|
|
729
|
+
title = @config.format_pr_title(task_data)
|
|
730
|
+
|
|
731
|
+
# Resolve base branch - handle SHA vs branch name
|
|
732
|
+
base = resolve_pr_base(start_point, options)
|
|
733
|
+
|
|
734
|
+
# Create draft PR
|
|
735
|
+
@pr_creator.create_draft(
|
|
736
|
+
branch: branch,
|
|
737
|
+
base: base,
|
|
738
|
+
title: title
|
|
739
|
+
)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Resolve PR base branch from start_point
|
|
743
|
+
#
|
|
744
|
+
# If start_point is a commit SHA (not a branch name), creates a branch
|
|
745
|
+
# on remote for that SHA to use as PR base.
|
|
746
|
+
#
|
|
747
|
+
# @param start_point [String, nil] Branch name or commit SHA
|
|
748
|
+
# @param options [Hash] Options (may include :push_remote)
|
|
749
|
+
# @return [String] Branch name to use as PR base
|
|
750
|
+
def resolve_pr_base(start_point, options)
|
|
751
|
+
return "main" unless start_point
|
|
752
|
+
|
|
753
|
+
# SHA patterns: 40 hex chars (full) or 7+ hex chars (abbreviated)
|
|
754
|
+
if start_point.match?(/\A[0-9a-f]{7,40}\z/i)
|
|
755
|
+
# start_point is a commit SHA - create a branch on remote for it
|
|
756
|
+
create_remote_branch_for_sha(start_point, options)
|
|
757
|
+
else
|
|
758
|
+
start_point
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Create a branch on remote for a commit SHA
|
|
763
|
+
#
|
|
764
|
+
# @param sha [String] Commit SHA
|
|
765
|
+
# @param options [Hash] Options (may include :push_remote)
|
|
766
|
+
# @return [String] Branch name (either new branch or "main" on failure)
|
|
767
|
+
def create_remote_branch_for_sha(sha, options)
|
|
768
|
+
base_branch = "base-#{sha[0..6]}"
|
|
769
|
+
remote = options[:push_remote] || @config.push_remote || "origin"
|
|
770
|
+
|
|
771
|
+
# Push the SHA as a new branch: git push origin SHA:refs/heads/base-abc1234
|
|
772
|
+
result = Atoms::GitCommand.execute("push", remote, "#{sha}:refs/heads/#{base_branch}")
|
|
773
|
+
|
|
774
|
+
if result[:success]
|
|
775
|
+
base_branch
|
|
776
|
+
else
|
|
777
|
+
warn "Warning: Failed to create base branch for SHA #{sha}, using 'main' as PR base"
|
|
778
|
+
"main"
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Save PR metadata to task file
|
|
783
|
+
#
|
|
784
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
785
|
+
# @param pr_result [Hash] PR creation result with :pr_number, :pr_url
|
|
786
|
+
# @return [Boolean] true if successful
|
|
787
|
+
def save_pr_to_task(task_data, pr_result)
|
|
788
|
+
task_id = extract_task_id(task_data)
|
|
789
|
+
|
|
790
|
+
pr_data = {
|
|
791
|
+
number: pr_result[:pr_number],
|
|
792
|
+
url: pr_result[:pr_url],
|
|
793
|
+
created_at: Time.now
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
@task_status_updater.add_pr_metadata(task_id, pr_data)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# Add started_at timestamp to task file IN WORKTREE
|
|
800
|
+
#
|
|
801
|
+
# This creates an initial commit in the worktree branch, enabling PR creation
|
|
802
|
+
# (GitHub requires at least one commit difference between branches for a PR).
|
|
803
|
+
#
|
|
804
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
805
|
+
# @param worktree_result [Hash] Worktree creation result with :worktree_path
|
|
806
|
+
# @param options [Hash] Options (may include :push_remote)
|
|
807
|
+
# @return [Hash] Result with :success, :error
|
|
808
|
+
def add_started_timestamp_in_worktree(task_data, worktree_result, options)
|
|
809
|
+
worktree_path = worktree_result[:worktree_path]
|
|
810
|
+
task_id = extract_task_id(task_data)
|
|
811
|
+
remote = options[:push_remote] || @config.push_remote || "origin"
|
|
812
|
+
|
|
813
|
+
Dir.chdir(worktree_path) do
|
|
814
|
+
# Set PROJECT_ROOT_PATH to worktree so TaskManager updates the right files
|
|
815
|
+
# (otherwise it finds and updates the main project's task files)
|
|
816
|
+
original_project_root = ENV["PROJECT_ROOT_PATH"]
|
|
817
|
+
ENV["PROJECT_ROOT_PATH"] = worktree_path
|
|
818
|
+
|
|
819
|
+
begin
|
|
820
|
+
# Update task file with started_at
|
|
821
|
+
if @task_status_updater.add_started_at_timestamp(task_id)
|
|
822
|
+
# Commit the change
|
|
823
|
+
if @task_committer.commit_all_changes("started", task_id)
|
|
824
|
+
# Push to remote
|
|
825
|
+
result = @task_pusher.push(remote: remote)
|
|
826
|
+
return result
|
|
827
|
+
else
|
|
828
|
+
return {success: false, error: "Failed to commit started_at change"}
|
|
829
|
+
end
|
|
830
|
+
else
|
|
831
|
+
return {success: false, error: "Failed to update task file with started_at"}
|
|
832
|
+
end
|
|
833
|
+
ensure
|
|
834
|
+
# Restore original PROJECT_ROOT_PATH
|
|
835
|
+
if original_project_root
|
|
836
|
+
ENV["PROJECT_ROOT_PATH"] = original_project_root
|
|
837
|
+
else
|
|
838
|
+
ENV.delete("PROJECT_ROOT_PATH")
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
rescue => e
|
|
843
|
+
{success: false, error: e.message}
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Create success workflow result
|
|
847
|
+
#
|
|
848
|
+
# @param message [String] Success message
|
|
849
|
+
# @param workflow_result [Hash] Workflow result to update
|
|
850
|
+
# @return [Hash] Updated workflow result
|
|
851
|
+
def success_workflow_result(message, workflow_result)
|
|
852
|
+
workflow_result.merge(
|
|
853
|
+
success: true,
|
|
854
|
+
message: message
|
|
855
|
+
)
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# Create error workflow result
|
|
859
|
+
#
|
|
860
|
+
# @param error_message [String] Error message
|
|
861
|
+
# @param workflow_result [Hash] Workflow result to update
|
|
862
|
+
# @return [Hash] Updated workflow result
|
|
863
|
+
def error_workflow_result(error_message, workflow_result)
|
|
864
|
+
workflow_result.merge(
|
|
865
|
+
success: false,
|
|
866
|
+
error: error_message
|
|
867
|
+
)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
# Extract task ID from task data
|
|
871
|
+
#
|
|
872
|
+
# @param task_data [Hash] Task data hash
|
|
873
|
+
# @return [String] Task ID (e.g., "094")
|
|
874
|
+
def extract_task_id(task_data)
|
|
875
|
+
# Use shared extractor that preserves subtask IDs (e.g., "121.01")
|
|
876
|
+
Atoms::TaskIDExtractor.extract(task_data)
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
# Get relative task path from absolute path
|
|
880
|
+
#
|
|
881
|
+
# Extracts the relative path portion from an absolute task path.
|
|
882
|
+
# E.g., "/project/.ace-task/v.0.9.0/tasks/145-feat/" -> ".ace-task/v.0.9.0/tasks/145-feat/"
|
|
883
|
+
#
|
|
884
|
+
# @param absolute_path [String] Absolute path to task directory
|
|
885
|
+
# @return [String] Relative path from project root
|
|
886
|
+
def relative_task_path(absolute_path)
|
|
887
|
+
return absolute_path unless absolute_path&.start_with?("/")
|
|
888
|
+
|
|
889
|
+
Pathname.new(absolute_path).relative_path_from(Pathname.new(@project_root)).to_s
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
# Create error result
|
|
893
|
+
#
|
|
894
|
+
# @param message [String] Error message
|
|
895
|
+
# @return [Hash] Error result
|
|
896
|
+
def error_result(message)
|
|
897
|
+
{
|
|
898
|
+
success: false,
|
|
899
|
+
error: message
|
|
900
|
+
}
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
end
|