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.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/assign/catalog/composition-rules.yml +211 -0
  3. data/.ace-defaults/assign/catalog/recipes/batch-tasks.recipe.yml +44 -0
  4. data/.ace-defaults/assign/catalog/recipes/documentation.recipe.yml +35 -0
  5. data/.ace-defaults/assign/catalog/recipes/fix-and-review.recipe.yml +32 -0
  6. data/.ace-defaults/assign/catalog/recipes/implement-simple.recipe.yml +29 -0
  7. data/.ace-defaults/assign/catalog/recipes/implement-with-pr.recipe.yml +48 -0
  8. data/.ace-defaults/assign/catalog/recipes/release-only.recipe.yml +34 -0
  9. data/.ace-defaults/assign/catalog/steps/apply-feedback.step.yml +22 -0
  10. data/.ace-defaults/assign/catalog/steps/commit.step.yml +22 -0
  11. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +28 -0
  12. data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +22 -0
  13. data/.ace-defaults/assign/catalog/steps/fix-tests.step.yml +22 -0
  14. data/.ace-defaults/assign/catalog/steps/lint.step.yml +22 -0
  15. data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +57 -0
  16. data/.ace-defaults/assign/catalog/steps/onboard-base.step.yml +19 -0
  17. data/.ace-defaults/assign/catalog/steps/onboard.step.yml +19 -0
  18. data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +17 -0
  19. data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +34 -0
  20. data/.ace-defaults/assign/catalog/steps/push-to-remote.step.yml +28 -0
  21. data/.ace-defaults/assign/catalog/steps/rebase-with-main.step.yml +28 -0
  22. data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +57 -0
  23. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +23 -0
  24. data/.ace-defaults/assign/catalog/steps/release.step.yml +23 -0
  25. data/.ace-defaults/assign/catalog/steps/reorganize-commits.step.yml +28 -0
  26. data/.ace-defaults/assign/catalog/steps/research.step.yml +19 -0
  27. data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +22 -0
  28. data/.ace-defaults/assign/catalog/steps/security-audit.step.yml +22 -0
  29. data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +25 -0
  30. data/.ace-defaults/assign/catalog/steps/squash-changelog.step.yml +28 -0
  31. data/.ace-defaults/assign/catalog/steps/task-load.step.yml +29 -0
  32. data/.ace-defaults/assign/catalog/steps/update-docs.step.yml +38 -0
  33. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +28 -0
  34. data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +42 -0
  35. data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +48 -0
  36. data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +36 -0
  37. data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +23 -0
  38. data/.ace-defaults/assign/config.yml +48 -0
  39. data/.ace-defaults/assign/presets/fix-bug.yml +65 -0
  40. data/.ace-defaults/assign/presets/quick-implement.yml +41 -0
  41. data/.ace-defaults/assign/presets/release-only.yml +35 -0
  42. data/.ace-defaults/assign/presets/work-on-docs.yml +41 -0
  43. data/.ace-defaults/assign/presets/work-on-task.yml +179 -0
  44. data/.ace-defaults/nav/protocols/skill-sources/ace-assign.yml +19 -0
  45. data/.ace-defaults/nav/protocols/wfi-sources/ace-assign.yml +19 -0
  46. data/CHANGELOG.md +1415 -0
  47. data/README.md +87 -0
  48. data/Rakefile +16 -0
  49. data/docs/exit-codes.md +61 -0
  50. data/docs/getting-started.md +121 -0
  51. data/docs/handbook.md +40 -0
  52. data/docs/usage.md +224 -0
  53. data/exe/ace-assign +16 -0
  54. data/handbook/guides/fork-context.g.md +231 -0
  55. data/handbook/skills/as-assign-compose/SKILL.md +24 -0
  56. data/handbook/skills/as-assign-create/SKILL.md +23 -0
  57. data/handbook/skills/as-assign-drive/SKILL.md +24 -0
  58. data/handbook/skills/as-assign-prepare/SKILL.md +23 -0
  59. data/handbook/skills/as-assign-recover-fork/SKILL.md +22 -0
  60. data/handbook/skills/as-assign-run-in-batches/SKILL.md +23 -0
  61. data/handbook/skills/as-assign-start/SKILL.md +25 -0
  62. data/handbook/workflow-instructions/assign/compose.wf.md +256 -0
  63. data/handbook/workflow-instructions/assign/create.wf.md +215 -0
  64. data/handbook/workflow-instructions/assign/drive.wf.md +666 -0
  65. data/handbook/workflow-instructions/assign/prepare.wf.md +469 -0
  66. data/handbook/workflow-instructions/assign/recover-fork.wf.md +233 -0
  67. data/handbook/workflow-instructions/assign/run-in-batches.wf.md +212 -0
  68. data/handbook/workflow-instructions/assign/start.wf.md +46 -0
  69. data/lib/ace/assign/atoms/assign_frontmatter_parser.rb +173 -0
  70. data/lib/ace/assign/atoms/catalog_loader.rb +101 -0
  71. data/lib/ace/assign/atoms/composition_rules.rb +219 -0
  72. data/lib/ace/assign/atoms/number_generator.rb +110 -0
  73. data/lib/ace/assign/atoms/preset_expander.rb +277 -0
  74. data/lib/ace/assign/atoms/step_file_parser.rb +207 -0
  75. data/lib/ace/assign/atoms/step_numbering.rb +227 -0
  76. data/lib/ace/assign/atoms/step_sorter.rb +66 -0
  77. data/lib/ace/assign/atoms/tree_formatter.rb +106 -0
  78. data/lib/ace/assign/cli/commands/add.rb +102 -0
  79. data/lib/ace/assign/cli/commands/assignment_target.rb +55 -0
  80. data/lib/ace/assign/cli/commands/create.rb +63 -0
  81. data/lib/ace/assign/cli/commands/fail.rb +43 -0
  82. data/lib/ace/assign/cli/commands/finish.rb +88 -0
  83. data/lib/ace/assign/cli/commands/fork_run.rb +229 -0
  84. data/lib/ace/assign/cli/commands/list.rb +166 -0
  85. data/lib/ace/assign/cli/commands/retry_cmd.rb +42 -0
  86. data/lib/ace/assign/cli/commands/select.rb +45 -0
  87. data/lib/ace/assign/cli/commands/start.rb +40 -0
  88. data/lib/ace/assign/cli/commands/status.rb +407 -0
  89. data/lib/ace/assign/cli.rb +144 -0
  90. data/lib/ace/assign/models/assignment.rb +107 -0
  91. data/lib/ace/assign/models/assignment_info.rb +66 -0
  92. data/lib/ace/assign/models/queue_state.rb +326 -0
  93. data/lib/ace/assign/models/step.rb +197 -0
  94. data/lib/ace/assign/molecules/assignment_discoverer.rb +57 -0
  95. data/lib/ace/assign/molecules/assignment_manager.rb +276 -0
  96. data/lib/ace/assign/molecules/fork_session_launcher.rb +102 -0
  97. data/lib/ace/assign/molecules/queue_scanner.rb +130 -0
  98. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +376 -0
  99. data/lib/ace/assign/molecules/step_renumberer.rb +227 -0
  100. data/lib/ace/assign/molecules/step_writer.rb +246 -0
  101. data/lib/ace/assign/organisms/assignment_executor.rb +1299 -0
  102. data/lib/ace/assign/version.rb +7 -0
  103. data/lib/ace/assign.rb +141 -0
  104. 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