ace-assign 0.37.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/assign/catalog/composition-rules.yml +211 -0
- data/.ace-defaults/assign/catalog/recipes/batch-tasks.recipe.yml +44 -0
- data/.ace-defaults/assign/catalog/recipes/documentation.recipe.yml +35 -0
- data/.ace-defaults/assign/catalog/recipes/fix-and-review.recipe.yml +32 -0
- data/.ace-defaults/assign/catalog/recipes/implement-simple.recipe.yml +29 -0
- data/.ace-defaults/assign/catalog/recipes/implement-with-pr.recipe.yml +48 -0
- data/.ace-defaults/assign/catalog/recipes/release-only.recipe.yml +34 -0
- data/.ace-defaults/assign/catalog/steps/apply-feedback.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/commit.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/fix-tests.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/lint.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +57 -0
- data/.ace-defaults/assign/catalog/steps/onboard-base.step.yml +19 -0
- data/.ace-defaults/assign/catalog/steps/onboard.step.yml +19 -0
- data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +17 -0
- data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +34 -0
- data/.ace-defaults/assign/catalog/steps/push-to-remote.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/rebase-with-main.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +57 -0
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +23 -0
- data/.ace-defaults/assign/catalog/steps/release.step.yml +23 -0
- data/.ace-defaults/assign/catalog/steps/reorganize-commits.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/research.step.yml +19 -0
- data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/security-audit.step.yml +22 -0
- data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +25 -0
- data/.ace-defaults/assign/catalog/steps/squash-changelog.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/task-load.step.yml +29 -0
- data/.ace-defaults/assign/catalog/steps/update-docs.step.yml +38 -0
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +28 -0
- data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +42 -0
- data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +48 -0
- data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +36 -0
- data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +23 -0
- data/.ace-defaults/assign/config.yml +48 -0
- data/.ace-defaults/assign/presets/fix-bug.yml +65 -0
- data/.ace-defaults/assign/presets/quick-implement.yml +41 -0
- data/.ace-defaults/assign/presets/release-only.yml +35 -0
- data/.ace-defaults/assign/presets/work-on-docs.yml +41 -0
- data/.ace-defaults/assign/presets/work-on-task.yml +179 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-assign.yml +19 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-assign.yml +19 -0
- data/CHANGELOG.md +1415 -0
- data/README.md +87 -0
- data/Rakefile +16 -0
- data/docs/exit-codes.md +61 -0
- data/docs/getting-started.md +121 -0
- data/docs/handbook.md +40 -0
- data/docs/usage.md +224 -0
- data/exe/ace-assign +16 -0
- data/handbook/guides/fork-context.g.md +231 -0
- data/handbook/skills/as-assign-compose/SKILL.md +24 -0
- data/handbook/skills/as-assign-create/SKILL.md +23 -0
- data/handbook/skills/as-assign-drive/SKILL.md +24 -0
- data/handbook/skills/as-assign-prepare/SKILL.md +23 -0
- data/handbook/skills/as-assign-recover-fork/SKILL.md +22 -0
- data/handbook/skills/as-assign-run-in-batches/SKILL.md +23 -0
- data/handbook/skills/as-assign-start/SKILL.md +25 -0
- data/handbook/workflow-instructions/assign/compose.wf.md +256 -0
- data/handbook/workflow-instructions/assign/create.wf.md +215 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +666 -0
- data/handbook/workflow-instructions/assign/prepare.wf.md +469 -0
- data/handbook/workflow-instructions/assign/recover-fork.wf.md +233 -0
- data/handbook/workflow-instructions/assign/run-in-batches.wf.md +212 -0
- data/handbook/workflow-instructions/assign/start.wf.md +46 -0
- data/lib/ace/assign/atoms/assign_frontmatter_parser.rb +173 -0
- data/lib/ace/assign/atoms/catalog_loader.rb +101 -0
- data/lib/ace/assign/atoms/composition_rules.rb +219 -0
- data/lib/ace/assign/atoms/number_generator.rb +110 -0
- data/lib/ace/assign/atoms/preset_expander.rb +277 -0
- data/lib/ace/assign/atoms/step_file_parser.rb +207 -0
- data/lib/ace/assign/atoms/step_numbering.rb +227 -0
- data/lib/ace/assign/atoms/step_sorter.rb +66 -0
- data/lib/ace/assign/atoms/tree_formatter.rb +106 -0
- data/lib/ace/assign/cli/commands/add.rb +102 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +55 -0
- data/lib/ace/assign/cli/commands/create.rb +63 -0
- data/lib/ace/assign/cli/commands/fail.rb +43 -0
- data/lib/ace/assign/cli/commands/finish.rb +88 -0
- data/lib/ace/assign/cli/commands/fork_run.rb +229 -0
- data/lib/ace/assign/cli/commands/list.rb +166 -0
- data/lib/ace/assign/cli/commands/retry_cmd.rb +42 -0
- data/lib/ace/assign/cli/commands/select.rb +45 -0
- data/lib/ace/assign/cli/commands/start.rb +40 -0
- data/lib/ace/assign/cli/commands/status.rb +407 -0
- data/lib/ace/assign/cli.rb +144 -0
- data/lib/ace/assign/models/assignment.rb +107 -0
- data/lib/ace/assign/models/assignment_info.rb +66 -0
- data/lib/ace/assign/models/queue_state.rb +326 -0
- data/lib/ace/assign/models/step.rb +197 -0
- data/lib/ace/assign/molecules/assignment_discoverer.rb +57 -0
- data/lib/ace/assign/molecules/assignment_manager.rb +276 -0
- data/lib/ace/assign/molecules/fork_session_launcher.rb +102 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +130 -0
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +376 -0
- data/lib/ace/assign/molecules/step_renumberer.rb +227 -0
- data/lib/ace/assign/molecules/step_writer.rb +246 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +1299 -0
- data/lib/ace/assign/version.rb +7 -0
- data/lib/ace/assign.rb +141 -0
- metadata +289 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Assign
|
|
8
|
+
module Molecules
|
|
9
|
+
# Writes and updates step markdown files.
|
|
10
|
+
#
|
|
11
|
+
# Handles creation of new step files and updating existing ones,
|
|
12
|
+
# including appending reports and updating frontmatter.
|
|
13
|
+
class StepWriter
|
|
14
|
+
# Create a new step file
|
|
15
|
+
#
|
|
16
|
+
# @param steps_dir [String] Path to steps directory
|
|
17
|
+
# @param number [String] Step number
|
|
18
|
+
# @param name [String] Step name
|
|
19
|
+
# @param instructions [String] Step instructions
|
|
20
|
+
# @param status [Symbol] Initial status
|
|
21
|
+
# @param added_by [String, nil] How step was added
|
|
22
|
+
# @param parent [String, nil] Parent step number
|
|
23
|
+
# @return [String] Path to created file
|
|
24
|
+
def create(steps_dir:, number:, name:, instructions:, status: :pending,
|
|
25
|
+
added_by: nil, parent: nil, extra: {})
|
|
26
|
+
filename = Atoms::StepFileParser.generate_filename(number, name)
|
|
27
|
+
file_path = File.join(steps_dir, filename)
|
|
28
|
+
|
|
29
|
+
frontmatter = {
|
|
30
|
+
"name" => name,
|
|
31
|
+
"status" => status.to_s
|
|
32
|
+
}
|
|
33
|
+
frontmatter["added_by"] = added_by if added_by
|
|
34
|
+
frontmatter["parent"] = parent if parent
|
|
35
|
+
frontmatter.merge!(extra.transform_keys(&:to_s)) if extra&.any?
|
|
36
|
+
|
|
37
|
+
content = build_file_content(frontmatter, instructions)
|
|
38
|
+
atomic_write(file_path, content)
|
|
39
|
+
|
|
40
|
+
file_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Update step frontmatter
|
|
44
|
+
#
|
|
45
|
+
# @param file_path [String] Path to step file
|
|
46
|
+
# @param updates [Hash] Frontmatter updates
|
|
47
|
+
# @return [String] Updated file path
|
|
48
|
+
def update_frontmatter(file_path, updates)
|
|
49
|
+
content = File.read(file_path)
|
|
50
|
+
parsed = Atoms::StepFileParser.parse(content)
|
|
51
|
+
|
|
52
|
+
# Merge updates into frontmatter
|
|
53
|
+
new_frontmatter = parsed[:frontmatter].merge(updates.transform_keys(&:to_s))
|
|
54
|
+
|
|
55
|
+
# Rebuild file
|
|
56
|
+
new_content = build_file_content(new_frontmatter, parsed[:body])
|
|
57
|
+
atomic_write(file_path, new_content)
|
|
58
|
+
|
|
59
|
+
file_path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Mark step as in progress
|
|
63
|
+
#
|
|
64
|
+
# @param file_path [String] Path to step file
|
|
65
|
+
# @return [String] Updated file path
|
|
66
|
+
def mark_in_progress(file_path)
|
|
67
|
+
update_frontmatter(file_path, {
|
|
68
|
+
"status" => "in_progress",
|
|
69
|
+
"started_at" => Time.now.utc.iso8601
|
|
70
|
+
})
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Mark step as pending again after it becomes blocked by newly added children.
|
|
74
|
+
#
|
|
75
|
+
# @param file_path [String] Path to step file
|
|
76
|
+
# @return [String] Updated file path
|
|
77
|
+
def mark_pending(file_path)
|
|
78
|
+
update_frontmatter(file_path, {
|
|
79
|
+
"status" => "pending",
|
|
80
|
+
"started_at" => nil,
|
|
81
|
+
"completed_at" => nil,
|
|
82
|
+
"error" => nil,
|
|
83
|
+
"stall_reason" => nil
|
|
84
|
+
})
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Mark step as done with report
|
|
88
|
+
#
|
|
89
|
+
# @param file_path [String] Path to step file
|
|
90
|
+
# @param report_content [String] Report content to write
|
|
91
|
+
# @param reports_dir [String] Path to reports directory
|
|
92
|
+
# @return [String] Updated file path
|
|
93
|
+
# @raise [ArgumentError] if report_content is nil or empty
|
|
94
|
+
def mark_done(file_path, report_content:, reports_dir:)
|
|
95
|
+
# Validate report content
|
|
96
|
+
raise ArgumentError, "Report content cannot be nil" if report_content.nil?
|
|
97
|
+
raise ArgumentError, "Report content cannot be empty" if report_content.strip.empty?
|
|
98
|
+
|
|
99
|
+
content = File.read(file_path)
|
|
100
|
+
parsed = Atoms::StepFileParser.parse(content)
|
|
101
|
+
|
|
102
|
+
# Extract number and name from filename for report file
|
|
103
|
+
filename_info = Atoms::StepFileParser.parse_filename(File.basename(file_path))
|
|
104
|
+
|
|
105
|
+
# Update frontmatter only (status + completed_at)
|
|
106
|
+
new_frontmatter = parsed[:frontmatter].merge({
|
|
107
|
+
"status" => "done",
|
|
108
|
+
"completed_at" => Time.now.utc.iso8601
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Write step file with updated frontmatter
|
|
112
|
+
new_content = build_file_content(new_frontmatter, parsed[:body])
|
|
113
|
+
atomic_write(file_path, new_content)
|
|
114
|
+
|
|
115
|
+
# Write report to separate file
|
|
116
|
+
report_filename = Atoms::StepFileParser.generate_report_filename(
|
|
117
|
+
filename_info[:number],
|
|
118
|
+
filename_info[:name]
|
|
119
|
+
)
|
|
120
|
+
report_path = File.join(reports_dir, report_filename)
|
|
121
|
+
|
|
122
|
+
write_report(report_path, filename_info[:number], filename_info[:name], report_content)
|
|
123
|
+
|
|
124
|
+
file_path
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Mark step as failed
|
|
128
|
+
#
|
|
129
|
+
# @param file_path [String] Path to step file
|
|
130
|
+
# @param error_message [String] Error message
|
|
131
|
+
# @return [String] Updated file path
|
|
132
|
+
def mark_failed(file_path, error_message:)
|
|
133
|
+
update_frontmatter(file_path, {
|
|
134
|
+
"status" => "failed",
|
|
135
|
+
"completed_at" => Time.now.utc.iso8601,
|
|
136
|
+
"error" => error_message
|
|
137
|
+
})
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Record fork execution PID metadata on a step.
|
|
141
|
+
#
|
|
142
|
+
# @param file_path [String] Path to fork root step file
|
|
143
|
+
# @param launch_pid [Integer] PID of launcher process
|
|
144
|
+
# @param tracked_pids [Array<Integer>] Observed subprocess/descendant PIDs
|
|
145
|
+
# @return [String] Updated file path
|
|
146
|
+
def record_fork_pid_info(file_path, launch_pid:, tracked_pids:, pid_file: nil)
|
|
147
|
+
update_frontmatter(file_path, {
|
|
148
|
+
"fork_launch_pid" => launch_pid.to_i,
|
|
149
|
+
"fork_tracked_pids" => Array(tracked_pids).map(&:to_i).uniq.sort,
|
|
150
|
+
"fork_pid_updated_at" => Time.now.utc.iso8601,
|
|
151
|
+
"fork_pid_file" => pid_file
|
|
152
|
+
})
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Append report content to step file
|
|
156
|
+
#
|
|
157
|
+
# @param file_path [String] Path to step file
|
|
158
|
+
# @param report_content [String] Report content to append
|
|
159
|
+
# @param reports_dir [String] Path to reports directory
|
|
160
|
+
# @return [String] Updated file path
|
|
161
|
+
def append_report(file_path, report_content, reports_dir:)
|
|
162
|
+
# Extract number and name from filename for report file
|
|
163
|
+
filename_info = Atoms::StepFileParser.parse_filename(File.basename(file_path))
|
|
164
|
+
|
|
165
|
+
# Generate report filename
|
|
166
|
+
report_filename = Atoms::StepFileParser.generate_report_filename(
|
|
167
|
+
filename_info[:number],
|
|
168
|
+
filename_info[:name]
|
|
169
|
+
)
|
|
170
|
+
report_path = File.join(reports_dir, report_filename)
|
|
171
|
+
|
|
172
|
+
# Check if report file exists
|
|
173
|
+
if File.exist?(report_path) && File.size(report_path) > 0
|
|
174
|
+
# Append to existing report with file locking
|
|
175
|
+
File.open(report_path, File::RDWR) do |f|
|
|
176
|
+
f.flock(File::LOCK_EX)
|
|
177
|
+
existing_content = f.read
|
|
178
|
+
# Find the end of the frontmatter and append after it
|
|
179
|
+
match = existing_content.match(/\n---\s*\n/)
|
|
180
|
+
if match
|
|
181
|
+
insertion_point = match.end(0)
|
|
182
|
+
new_content = existing_content[0...insertion_point] + report_content + "\n" + existing_content[insertion_point..]
|
|
183
|
+
else
|
|
184
|
+
new_content = existing_content + "\n" + report_content
|
|
185
|
+
end
|
|
186
|
+
# Rewrite content in-place on locked file descriptor
|
|
187
|
+
# This preserves the POSIX lock (rename would break it by replacing inode)
|
|
188
|
+
f.rewind
|
|
189
|
+
f.truncate(0)
|
|
190
|
+
f.write(new_content)
|
|
191
|
+
f.flush
|
|
192
|
+
fsync_after_write(f)
|
|
193
|
+
end
|
|
194
|
+
else
|
|
195
|
+
# Create new report file
|
|
196
|
+
write_report(report_path, filename_info[:number], filename_info[:name], report_content)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
file_path
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
# Write content atomically using temp file + rename pattern.
|
|
205
|
+
# Prevents partial writes if process crashes mid-write.
|
|
206
|
+
def atomic_write(path, content)
|
|
207
|
+
temp_path = "#{path}.tmp.#{Process.pid}"
|
|
208
|
+
File.write(temp_path, content)
|
|
209
|
+
File.rename(temp_path, path)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Sync file to disk after write to ensure data persistence.
|
|
213
|
+
# Especially important when rewriting in-place under file lock.
|
|
214
|
+
#
|
|
215
|
+
# @param file [File] File object to sync
|
|
216
|
+
def fsync_after_write(file)
|
|
217
|
+
file.fsync
|
|
218
|
+
rescue IOError
|
|
219
|
+
# fsync may not be supported on all file systems (e.g., NFS)
|
|
220
|
+
# Gracefully degrade if not available
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_file_content(frontmatter, body)
|
|
224
|
+
yaml = frontmatter.compact.to_yaml
|
|
225
|
+
"#{yaml}---\n\n#{body}\n"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Write report to separate file with YAML frontmatter
|
|
229
|
+
# @param report_path [String] Path to report file
|
|
230
|
+
# @param number [String] Step number
|
|
231
|
+
# @param name [String] Step name
|
|
232
|
+
# @param content [String] Report content
|
|
233
|
+
def write_report(report_path, number, name, content)
|
|
234
|
+
frontmatter = {
|
|
235
|
+
"step" => number,
|
|
236
|
+
"name" => name,
|
|
237
|
+
"completed_at" => Time.now.utc.iso8601
|
|
238
|
+
}
|
|
239
|
+
yaml = frontmatter.to_yaml
|
|
240
|
+
report_content = "#{yaml}---\n\n#{content}\n"
|
|
241
|
+
atomic_write(report_path, report_content)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|