ace-assign 0.42.4 → 0.55.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 +4 -4
- data/.ace-defaults/assign/catalog/composition-rules.yml +2 -17
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +0 -26
- data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +1 -2
- data/.ace-defaults/assign/catalog/steps/onboard.step.yml +0 -17
- data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +0 -11
- data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +3 -0
- data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +3 -2
- data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -16
- data/.ace-defaults/assign/catalog/steps/split-subtree-root.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/task-load.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +7 -34
- data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +7 -4
- data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -17
- data/.ace-defaults/assign/config.yml +1 -0
- data/.ace-defaults/assign/presets/fix-bug.yml +4 -3
- data/.ace-defaults/assign/presets/quick-implement.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +6 -16
- data/CHANGELOG.md +216 -0
- data/README.md +20 -43
- data/docs/demo/canonical-skill-source.gif +0 -0
- data/docs/demo/canonical-skill-source.tape.yml +51 -0
- data/docs/demo/fork-provider.cast +834 -0
- data/docs/demo/fork-provider.gif +0 -0
- data/docs/demo/fork-provider.recording.json +30 -0
- data/docs/demo/fork-provider.tape.yml +77 -20
- data/docs/getting-started.md +5 -2
- data/docs/usage.md +74 -4
- data/handbook/guides/fork-context.g.md +31 -7
- data/handbook/skills/as-assign-drive/SKILL.md +13 -1
- data/handbook/skills/as-create-retro-internal/SKILL.md +29 -0
- data/handbook/skills/as-mark-task-done-internal/SKILL.md +29 -0
- data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +30 -0
- data/handbook/skills/as-task-load-internal/SKILL.md +28 -0
- data/handbook/workflow-instructions/assign/compose.wf.md +3 -3
- data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +11 -0
- data/handbook/workflow-instructions/assign/create.wf.md +6 -3
- data/handbook/workflow-instructions/assign/drive.wf.md +330 -40
- data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +12 -0
- data/handbook/workflow-instructions/assign/prepare.wf.md +10 -5
- data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +14 -0
- data/handbook/workflow-instructions/assign/run-in-batches.wf.md +4 -1
- data/handbook/workflow-instructions/assign/start.wf.md +5 -2
- data/handbook/workflow-instructions/assign/task-load-internal.wf.md +12 -0
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +36 -0
- data/lib/ace/assign/atoms/catalog_loader.rb +105 -2
- data/lib/ace/assign/atoms/preset_expander.rb +4 -0
- data/lib/ace/assign/atoms/step_file_parser.rb +15 -0
- data/lib/ace/assign/atoms/tree_formatter.rb +2 -2
- data/lib/ace/assign/cli/commands/add.rb +20 -11
- data/lib/ace/assign/cli/commands/assignment_target.rb +87 -3
- data/lib/ace/assign/cli/commands/create.rb +1 -1
- data/lib/ace/assign/cli/commands/fail.rb +1 -1
- data/lib/ace/assign/cli/commands/finish.rb +32 -8
- data/lib/ace/assign/cli/commands/fork_run.rb +58 -16
- data/lib/ace/assign/cli/commands/fork_session.rb +52 -0
- data/lib/ace/assign/cli/commands/list.rb +4 -3
- data/lib/ace/assign/cli/commands/retry_cmd.rb +1 -1
- data/lib/ace/assign/cli/commands/start.rb +9 -3
- data/lib/ace/assign/cli/commands/status.rb +237 -230
- data/lib/ace/assign/cli/commands/step.rb +62 -0
- data/lib/ace/assign/cli.rb +8 -1
- data/lib/ace/assign/models/assignment_info.rb +33 -4
- data/lib/ace/assign/models/queue_state.rb +101 -39
- data/lib/ace/assign/models/step.rb +17 -5
- data/lib/ace/assign/molecules/fork_session_launcher.rb +218 -21
- data/lib/ace/assign/molecules/queue_scanner.rb +1 -0
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +223 -47
- data/lib/ace/assign/molecules/step_writer.rb +3 -3
- data/lib/ace/assign/molecules/tmux_control_surface_runner.rb +249 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +355 -106
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -0
- metadata +35 -5
- data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +0 -42
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# mark-task-done-internal
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Mark a task as done and verify the state transition persisted.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
1. Run `ace-task update <taskref> --set status=done --move-to archive --git-commit`.
|
|
10
|
+
2. Verify with `ace-task show <taskref>` and confirm `status: done`.
|
|
11
|
+
3. If the task has a parent, check whether siblings are all done; when true, mark the parent done and verify.
|
|
12
|
+
4. Repeat upward only while all siblings remain done.
|
|
@@ -141,6 +141,11 @@ For `--taskrefs 148,149,150`, expansion generates:
|
|
|
141
141
|
030 finalize
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
Generated batch parents should carry scheduler metadata:
|
|
145
|
+
|
|
146
|
+
- `010 batch-tasks (parent, auto-completes, batch_parent: true, parallel: false)`
|
|
147
|
+
- `fork_retry_limit: 1`
|
|
148
|
+
|
|
144
149
|
The hierarchical numbering enables:
|
|
145
150
|
- Parent auto-completion when all children are done
|
|
146
151
|
- Parent-only fork markers for subtree delegation
|
|
@@ -336,7 +341,7 @@ session:
|
|
|
336
341
|
|
|
337
342
|
steps:
|
|
338
343
|
- name: <step-name>
|
|
339
|
-
|
|
344
|
+
source: <source-reference> # Canonical: skill://... or wfi://...
|
|
340
345
|
instructions:
|
|
341
346
|
- <resolved instruction line>
|
|
342
347
|
# ... more steps
|
|
@@ -344,7 +349,7 @@ steps:
|
|
|
344
349
|
|
|
345
350
|
### 8. Output Result
|
|
346
351
|
|
|
347
|
-
Default output: `<task>/jobs/<timestamp>-job.yml` (e.g., `.ace-
|
|
352
|
+
Default output: `<task>/jobs/<timestamp>-job.yml` (e.g., `.ace-task/v.0.9.0/tasks/229-xxx/jobs/k5abc123-job.yml`)
|
|
348
353
|
|
|
349
354
|
Custom output: Use `--output path/to/custom.yaml`
|
|
350
355
|
|
|
@@ -379,19 +384,19 @@ session:
|
|
|
379
384
|
|
|
380
385
|
steps:
|
|
381
386
|
- name: onboard
|
|
382
|
-
|
|
387
|
+
source: skill://as-onboard
|
|
383
388
|
instructions:
|
|
384
389
|
- Onboard yourself to the codebase.
|
|
385
390
|
- Load context and understand the project structure.
|
|
386
391
|
|
|
387
392
|
- name: work-on-task
|
|
388
|
-
|
|
393
|
+
source: skill://as-task-work
|
|
389
394
|
instructions:
|
|
390
395
|
- Work on task 123.
|
|
391
396
|
- Implement the required changes following project conventions.
|
|
392
397
|
|
|
393
398
|
- name: create-pr
|
|
394
|
-
|
|
399
|
+
source: skill://as-github-pr-create
|
|
395
400
|
instructions:
|
|
396
401
|
- Create a pull request for the changes.
|
|
397
402
|
- Capture the PR number for subsequent review steps.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# reflect-and-refactor-internal
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Run architecture reflection and bounded refactoring before release/closeout.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
1. Validate implementation/demo state.
|
|
10
|
+
2. Run architecture-focused review on the active diff.
|
|
11
|
+
3. Categorize findings (refactor/accept/skip).
|
|
12
|
+
4. Execute bounded refactoring for selected findings only.
|
|
13
|
+
5. Commit refactor changes separately.
|
|
14
|
+
6. If a replan trigger is hit, inject follow-up implementation steps and rerun once.
|
|
@@ -3,7 +3,7 @@ doc-type: workflow
|
|
|
3
3
|
title: Run In Batches Workflow
|
|
4
4
|
purpose: workflow instruction for reusable repeated-item orchestration with deterministic assignment creation
|
|
5
5
|
ace-docs:
|
|
6
|
-
last-updated: 2026-
|
|
6
|
+
last-updated: 2026-04-07
|
|
7
7
|
last-checked: 2026-03-21
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -163,6 +163,8 @@ If `--run` is present:
|
|
|
163
163
|
/as-assign-drive <assignment-id>
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
This handoff must continue through the full batch execution, including all child fork subtrees, until the assignment is complete or an explicit blocker/failure stop condition is reached. Child completion is not a valid stop boundary.
|
|
167
|
+
|
|
166
168
|
If no workable step is available, keep creation successful and report why drive did not continue.
|
|
167
169
|
|
|
168
170
|
### 7. Report Result
|
|
@@ -203,6 +205,7 @@ Show:
|
|
|
203
205
|
- Parent/child metadata reflects scheduler intent (`parallel`, `max_parallel`, `fork_retry_limit`)
|
|
204
206
|
- `{{item}}` substitution and `Target item:` fallback are deterministic
|
|
205
207
|
- Optional `--run` handoff delegates to `/as-assign-drive`
|
|
208
|
+
- Optional `--run` handoff preserves drive's run-until-complete-or-blocked semantics across the whole batch
|
|
206
209
|
|
|
207
210
|
## Verification
|
|
208
211
|
|
|
@@ -3,7 +3,7 @@ doc-type: workflow
|
|
|
3
3
|
title: Start Assignment Workflow (Legacy Compatibility)
|
|
4
4
|
purpose: preserve compatibility for as-assign-start while routing to public assign/create + assign/drive flow
|
|
5
5
|
ace-docs:
|
|
6
|
-
last-updated: 2026-
|
|
6
|
+
last-updated: 2026-04-07
|
|
7
7
|
last-checked: 2026-03-21
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -39,8 +39,11 @@ Primary public UX remains:
|
|
|
39
39
|
/as-assign-drive <assignment-id>
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
When this handoff occurs, `/as-assign-drive` must continue until the assignment is complete or explicitly blocked. It is not a single-step progress probe.
|
|
43
|
+
|
|
42
44
|
## Success Criteria
|
|
43
45
|
|
|
44
46
|
- Preserves compatibility entrypoint for orchestration examples.
|
|
45
47
|
- Delegates behavior to `assign/create` and `assign/drive`.
|
|
46
|
-
- Does not redefine the public assignment flow.
|
|
48
|
+
- Does not redefine the public assignment flow.
|
|
49
|
+
- Preserves drive's run-until-complete-or-blocked semantics.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# task-load-internal
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Load task behavioral specification and dependency context for assignment execution.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
1. Run `ace-bundle task://<taskref>` for the target task reference.
|
|
10
|
+
2. If task dependencies are declared, run `ace-bundle task://<dep-ref>` for each dependency.
|
|
11
|
+
3. Review relevant dependency reports under `.ace-local/assign/` so the plan/work steps build on prior implementation.
|
|
12
|
+
4. Confirm context is loaded before proceeding.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: workflow
|
|
3
|
+
title: Assign Verify Test Suite Workflow
|
|
4
|
+
purpose: Narrow deterministic verification contract for ace-assign orchestration
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-04-13
|
|
7
|
+
last-checked: 2026-04-13
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Verify Test Suite Workflow
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Run the deterministic verification contract used by `ace-assign`:
|
|
15
|
+
- package-scoped `ace-test <package> all --profile 6` for modified packages
|
|
16
|
+
- monorepo verification with `ace-test-suite --target all`
|
|
17
|
+
|
|
18
|
+
Do not run `ace-test-e2e` or `ace-test-e2e-suite` from this workflow.
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
1. Detect whether modified files affect runnable code.
|
|
23
|
+
2. If changes are docs-only or otherwise non-runnable, skip with a clear reason.
|
|
24
|
+
3. Detect modified packages from the current diff or working tree.
|
|
25
|
+
4. Run `ace-test <package> all --profile 6` for each modified package.
|
|
26
|
+
5. Run `ace-test-suite --target all`.
|
|
27
|
+
|
|
28
|
+
## Skip Guidance
|
|
29
|
+
|
|
30
|
+
Skip only when all modified files are documentation, retrospectives, task specs, or similarly non-runnable metadata.
|
|
31
|
+
|
|
32
|
+
## Success Criteria
|
|
33
|
+
|
|
34
|
+
- All modified packages pass `ace-test <package> all --profile 6`
|
|
35
|
+
- `ace-test-suite --target all` passes
|
|
36
|
+
- No E2E commands are used as part of this step
|
|
@@ -21,13 +21,19 @@ module Ace
|
|
|
21
21
|
# Load all step definitions from a catalog directory.
|
|
22
22
|
#
|
|
23
23
|
# @param steps_dir [String] Path to catalog/steps/ directory
|
|
24
|
+
# @param canonical_steps [Array<Hash>, Symbol, Boolean, nil] Canonical
|
|
25
|
+
# step metadata to merge. `:auto` (default) loads from skill sources.
|
|
26
|
+
# `false` disables canonical merge and returns raw YAML definitions.
|
|
24
27
|
# @return [Array<Hash>] Array of step definition hashes
|
|
25
|
-
def self.load_all(steps_dir)
|
|
28
|
+
def self.load_all(steps_dir, canonical_steps: :auto)
|
|
26
29
|
return [] unless File.directory?(steps_dir)
|
|
27
30
|
|
|
28
|
-
Dir.glob(File.join(steps_dir, "*.step.yml")).sort.filter_map do |path|
|
|
31
|
+
yaml_steps = Dir.glob(File.join(steps_dir, "*.step.yml")).sort.filter_map do |path|
|
|
29
32
|
parse_step_file(path)
|
|
30
33
|
end
|
|
34
|
+
return [] if yaml_steps.empty?
|
|
35
|
+
|
|
36
|
+
merge_step_catalog(yaml_steps, resolve_canonical_steps(canonical_steps))
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
# Find a step definition by name.
|
|
@@ -94,6 +100,103 @@ module Ace
|
|
|
94
100
|
warn "Warning: Failed to parse step file #{path}: #{e.message}"
|
|
95
101
|
nil
|
|
96
102
|
end
|
|
103
|
+
|
|
104
|
+
def self.resolve_canonical_steps(canonical_steps)
|
|
105
|
+
case canonical_steps
|
|
106
|
+
when false
|
|
107
|
+
[]
|
|
108
|
+
when :auto, nil
|
|
109
|
+
begin
|
|
110
|
+
require_relative "../molecules/skill_assign_source_resolver"
|
|
111
|
+
Molecules::SkillAssignSourceResolver.new.assign_step_catalog
|
|
112
|
+
rescue LoadError, StandardError
|
|
113
|
+
[]
|
|
114
|
+
end
|
|
115
|
+
when Array
|
|
116
|
+
canonical_steps
|
|
117
|
+
else
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
private_class_method :resolve_canonical_steps
|
|
122
|
+
|
|
123
|
+
def self.merge_step_catalog(base_steps, override_steps)
|
|
124
|
+
index = {}
|
|
125
|
+
order = []
|
|
126
|
+
|
|
127
|
+
Array(base_steps).each do |step|
|
|
128
|
+
name = step["name"]
|
|
129
|
+
next if name.nil? || name.empty?
|
|
130
|
+
|
|
131
|
+
index[name] = step
|
|
132
|
+
order << name
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Array(override_steps).each do |step|
|
|
136
|
+
name = step["name"]
|
|
137
|
+
next if name.nil? || name.empty?
|
|
138
|
+
|
|
139
|
+
order << name unless index.key?(name)
|
|
140
|
+
index[name] = deep_merge_step_definition(index[name], step)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
order.map { |name| index[name] }.compact
|
|
144
|
+
end
|
|
145
|
+
private_class_method :merge_step_catalog
|
|
146
|
+
|
|
147
|
+
def self.deep_merge_step_definition(base, override)
|
|
148
|
+
return override unless base.is_a?(Hash)
|
|
149
|
+
return base unless override.is_a?(Hash)
|
|
150
|
+
|
|
151
|
+
merged = base.dup
|
|
152
|
+
override.each do |key, value|
|
|
153
|
+
if runtime_binding_override_key?(key, base, override)
|
|
154
|
+
merged[key] = base[key]
|
|
155
|
+
next
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
merged[key] =
|
|
159
|
+
if merged[key].is_a?(Hash) && value.is_a?(Hash)
|
|
160
|
+
deep_merge_step_definition(merged[key], value)
|
|
161
|
+
else
|
|
162
|
+
value
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
merged
|
|
166
|
+
end
|
|
167
|
+
private_class_method :deep_merge_step_definition
|
|
168
|
+
|
|
169
|
+
def self.runtime_binding_override_key?(key, base, override)
|
|
170
|
+
return false unless %w[source workflow skill source_skill].include?(key)
|
|
171
|
+
return false unless local_runtime_binding_present?(base)
|
|
172
|
+
canonical_binding_present?(override)
|
|
173
|
+
end
|
|
174
|
+
private_class_method :runtime_binding_override_key?
|
|
175
|
+
|
|
176
|
+
def self.local_runtime_binding_present?(entry)
|
|
177
|
+
entry.is_a?(Hash) && (
|
|
178
|
+
present_string?(entry["source"]) ||
|
|
179
|
+
present_string?(entry["workflow"]) ||
|
|
180
|
+
present_string?(entry["skill"])
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
private_class_method :local_runtime_binding_present?
|
|
184
|
+
|
|
185
|
+
def self.canonical_binding_present?(entry)
|
|
186
|
+
entry.is_a?(Hash) && (
|
|
187
|
+
present_string?(entry["source"]) ||
|
|
188
|
+
present_string?(entry["workflow"]) ||
|
|
189
|
+
present_string?(entry["skill"]) ||
|
|
190
|
+
present_string?(entry["source_skill"])
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
private_class_method :canonical_binding_present?
|
|
194
|
+
|
|
195
|
+
def self.present_string?(value)
|
|
196
|
+
value.is_a?(String) && !value.strip.empty?
|
|
197
|
+
end
|
|
198
|
+
private_class_method :present_string?
|
|
199
|
+
|
|
97
200
|
private_class_method :parse_step_file
|
|
98
201
|
end
|
|
99
202
|
end
|
|
@@ -192,6 +192,10 @@ module Ace
|
|
|
192
192
|
|
|
193
193
|
step["skill"] = config["skill"] if config["skill"]
|
|
194
194
|
step["context"] = config["context"] if config["context"]
|
|
195
|
+
step["batch_parent"] = config["batch_parent"] unless config["batch_parent"].nil?
|
|
196
|
+
step["parallel"] = config["parallel"] unless config["parallel"].nil?
|
|
197
|
+
step["max_parallel"] = config["max_parallel"] if config["max_parallel"]
|
|
198
|
+
step["fork_retry_limit"] = config["fork_retry_limit"] unless config["fork_retry_limit"].nil?
|
|
195
199
|
|
|
196
200
|
step
|
|
197
201
|
end
|
|
@@ -49,6 +49,7 @@ module Ace
|
|
|
49
49
|
{
|
|
50
50
|
name: fm["name"],
|
|
51
51
|
status: (fm["status"] || "pending").to_sym,
|
|
52
|
+
source: normalize_source(fm["source"], fm["workflow"], fm["skill"]),
|
|
52
53
|
skill: fm["skill"],
|
|
53
54
|
workflow: fm["workflow"],
|
|
54
55
|
context: context, # "fork" triggers Task tool execution
|
|
@@ -212,6 +213,20 @@ module Ace
|
|
|
212
213
|
parsed
|
|
213
214
|
end
|
|
214
215
|
private_class_method :parse_non_negative_integer
|
|
216
|
+
|
|
217
|
+
def self.normalize_source(source, workflow, skill)
|
|
218
|
+
source_ref = source.to_s.strip
|
|
219
|
+
return source_ref unless source_ref.empty?
|
|
220
|
+
|
|
221
|
+
workflow_ref = workflow.to_s.strip
|
|
222
|
+
return workflow_ref unless workflow_ref.empty?
|
|
223
|
+
|
|
224
|
+
skill_ref = skill.to_s.strip
|
|
225
|
+
return nil if skill_ref.empty?
|
|
226
|
+
|
|
227
|
+
"skill://#{skill_ref}"
|
|
228
|
+
end
|
|
229
|
+
private_class_method :normalize_source
|
|
215
230
|
end
|
|
216
231
|
end
|
|
217
232
|
end
|
|
@@ -14,14 +14,14 @@ module Ace
|
|
|
14
14
|
# # +-- onboard (completed)
|
|
15
15
|
# # +-- work-on-task (running)
|
|
16
16
|
# # | +-- onboard (completed)
|
|
17
|
-
# # | +-- implement (
|
|
17
|
+
# # | +-- implement (active)
|
|
18
18
|
# # | \-- verify-tests (pending)
|
|
19
19
|
# # \-- review-pr (pending)
|
|
20
20
|
module TreeFormatter
|
|
21
21
|
# State display labels matching list command
|
|
22
22
|
STATE_LABELS = {
|
|
23
23
|
pending: "pending",
|
|
24
|
-
|
|
24
|
+
active: "active",
|
|
25
25
|
running: "running",
|
|
26
26
|
paused: "paused",
|
|
27
27
|
completed: "completed",
|
|
@@ -35,11 +35,11 @@ module Ace
|
|
|
35
35
|
mode = selected_mode(options)
|
|
36
36
|
case mode
|
|
37
37
|
when :yaml
|
|
38
|
-
handle_yaml_mode(executor, options)
|
|
38
|
+
handle_yaml_mode(executor, options, target)
|
|
39
39
|
when :step
|
|
40
|
-
handle_step_mode(executor, options)
|
|
40
|
+
handle_step_mode(executor, options, target)
|
|
41
41
|
when :task
|
|
42
|
-
handle_task_mode(executor, options)
|
|
42
|
+
handle_task_mode(executor, options, target)
|
|
43
43
|
else
|
|
44
44
|
raise Ace::Support::Cli::Error, "Exactly one of --yaml, --step, or --task is required"
|
|
45
45
|
end
|
|
@@ -77,20 +77,21 @@ module Ace
|
|
|
77
77
|
insertion_modes(options).find { |_, value| !value.to_s.strip.empty? }&.first
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def handle_yaml_mode(executor, options)
|
|
80
|
+
def handle_yaml_mode(executor, options, target)
|
|
81
81
|
yaml_path = options[:yaml].to_s.strip
|
|
82
82
|
steps = load_steps_from_file(yaml_path)
|
|
83
83
|
result = executor.add_batch(
|
|
84
84
|
steps: steps,
|
|
85
85
|
after: options[:after],
|
|
86
86
|
as_child: options[:child],
|
|
87
|
-
source_file: yaml_path
|
|
87
|
+
source_file: yaml_path,
|
|
88
|
+
fork_root: target.scope
|
|
88
89
|
)
|
|
89
90
|
print_yaml_result(result, yaml_path) unless options[:quiet]
|
|
90
91
|
nil
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def handle_step_mode(executor, options)
|
|
94
|
+
def handle_step_mode(executor, options, target)
|
|
94
95
|
preset_name = resolve_preset_name(executor, options)
|
|
95
96
|
preset = Atoms::PresetLoader.load(preset_name)
|
|
96
97
|
requested = parse_step_names(options[:step])
|
|
@@ -111,7 +112,8 @@ module Ace
|
|
|
111
112
|
steps: [step],
|
|
112
113
|
after: options[:child] ? options[:after] : sibling_cursor,
|
|
113
114
|
as_child: options[:child],
|
|
114
|
-
source_file: "preset:#{preset_name}"
|
|
115
|
+
source_file: "preset:#{preset_name}",
|
|
116
|
+
fork_root: target.scope
|
|
115
117
|
)
|
|
116
118
|
|
|
117
119
|
inserted_added = Array(inserted[:added])
|
|
@@ -126,7 +128,7 @@ module Ace
|
|
|
126
128
|
nil
|
|
127
129
|
end
|
|
128
130
|
|
|
129
|
-
def handle_task_mode(executor, options)
|
|
131
|
+
def handle_task_mode(executor, options, target)
|
|
130
132
|
preset_name = resolve_preset_name(executor, options)
|
|
131
133
|
preset = Atoms::PresetLoader.load(preset_name)
|
|
132
134
|
task_ref = options[:task].to_s.strip
|
|
@@ -139,7 +141,8 @@ module Ace
|
|
|
139
141
|
end
|
|
140
142
|
|
|
141
143
|
parent_step = options[:after].to_s.strip
|
|
142
|
-
parent_step =
|
|
144
|
+
parent_step = target.scope.to_s.strip if parent_step.empty? && !target.scope.to_s.strip.empty?
|
|
145
|
+
parent_step = detect_batch_parent(executor, target) if parent_step.empty?
|
|
143
146
|
if parent_step.to_s.strip.empty?
|
|
144
147
|
raise Ace::Support::Cli::Error, "No batch parent found. Pass --after <step> to specify."
|
|
145
148
|
end
|
|
@@ -150,7 +153,8 @@ module Ace
|
|
|
150
153
|
steps: [task_step],
|
|
151
154
|
after: parent_step,
|
|
152
155
|
as_child: insert_as_child,
|
|
153
|
-
source_file: "preset:#{preset_name}:task:#{task_ref}"
|
|
156
|
+
source_file: "preset:#{preset_name}:task:#{task_ref}",
|
|
157
|
+
fork_root: target.scope
|
|
154
158
|
)
|
|
155
159
|
|
|
156
160
|
print_task_result(result, task_ref, parent_step, as_child: insert_as_child) unless options[:quiet]
|
|
@@ -235,8 +239,13 @@ module Ace
|
|
|
235
239
|
tokens.to_a.sort
|
|
236
240
|
end
|
|
237
241
|
|
|
238
|
-
def detect_batch_parent(executor)
|
|
242
|
+
def detect_batch_parent(executor, target)
|
|
239
243
|
state = executor.status[:state]
|
|
244
|
+
if target.scope && !target.scope.to_s.strip.empty?
|
|
245
|
+
scoped_steps = state.subtree_steps(target.scope.strip)
|
|
246
|
+
scoped_state = Ace::Assign::Models::QueueState.new(steps: scoped_steps, assignment: state.assignment)
|
|
247
|
+
state = scoped_state
|
|
248
|
+
end
|
|
240
249
|
|
|
241
250
|
batch_parent = state.top_level.find { |step| step.name == "batch-tasks" }
|
|
242
251
|
return batch_parent.number if batch_parent
|
|
@@ -10,17 +10,26 @@ module Ace
|
|
|
10
10
|
# - <assignment-id>
|
|
11
11
|
# - <assignment-id>@<step-number>
|
|
12
12
|
module AssignmentTarget
|
|
13
|
+
DEFAULT_TARGET_ENV = "ACE_ASSIGN_DEFAULT_TARGET"
|
|
13
14
|
Target = Struct.new(:assignment_id, :scope, keyword_init: true)
|
|
15
|
+
View = Struct.new(:assignment, :state, :scoped_state, :active_steps, :next_step, :focus_step, :scope_root, keyword_init: true)
|
|
14
16
|
|
|
15
17
|
private
|
|
16
18
|
|
|
17
19
|
def resolve_assignment_target(options)
|
|
18
20
|
assignment_raw = options[:assignment]
|
|
19
|
-
unless assignment_raw.nil? || assignment_raw.to_s.strip.empty?
|
|
20
|
-
|
|
21
|
+
explicit_target = unless assignment_raw.nil? || assignment_raw.to_s.strip.empty?
|
|
22
|
+
parse_assignment_target(assignment_raw)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
env_target = env_assignment_target
|
|
26
|
+
if explicit_target && env_target && target_identity(explicit_target) != target_identity(env_target)
|
|
27
|
+
raise Ace::Support::Cli::Error,
|
|
28
|
+
"Conflicting assignment targets: --assignment #{target_identity(explicit_target)} " \
|
|
29
|
+
"does not match #{DEFAULT_TARGET_ENV}=#{target_identity(env_target)}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
explicit_target || env_target || Target.new(assignment_id: nil, scope: nil)
|
|
24
33
|
end
|
|
25
34
|
|
|
26
35
|
def parse_assignment_target(raw)
|
|
@@ -37,6 +46,22 @@ module Ace
|
|
|
37
46
|
Target.new(assignment_id: assignment_id, scope: scope)
|
|
38
47
|
end
|
|
39
48
|
|
|
49
|
+
def env_assignment_target
|
|
50
|
+
raw = ENV[DEFAULT_TARGET_ENV].to_s.strip
|
|
51
|
+
return nil if raw.empty?
|
|
52
|
+
|
|
53
|
+
parse_assignment_target(raw)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def target_identity(target)
|
|
57
|
+
return "" unless target
|
|
58
|
+
|
|
59
|
+
scope = target.scope.to_s.strip
|
|
60
|
+
return target.assignment_id.to_s if scope.empty?
|
|
61
|
+
|
|
62
|
+
"#{target.assignment_id}@#{scope}"
|
|
63
|
+
end
|
|
64
|
+
|
|
40
65
|
def build_executor_for_target(target)
|
|
41
66
|
return Organisms::AssignmentExecutor.new unless target.assignment_id
|
|
42
67
|
|
|
@@ -48,6 +73,65 @@ module Ace
|
|
|
48
73
|
executor.assignment_manager.define_singleton_method(:find_active) { assignment }
|
|
49
74
|
executor
|
|
50
75
|
end
|
|
76
|
+
|
|
77
|
+
def resolve_assignment_view(target)
|
|
78
|
+
executor = build_executor_for_target(target)
|
|
79
|
+
result = executor.status
|
|
80
|
+
state = result[:state]
|
|
81
|
+
scoped = scoped_status_view(state, target.scope)
|
|
82
|
+
|
|
83
|
+
View.new(
|
|
84
|
+
assignment: result[:assignment],
|
|
85
|
+
state: state,
|
|
86
|
+
scoped_state: scoped[:state],
|
|
87
|
+
active_steps: scoped[:active_steps],
|
|
88
|
+
next_step: scoped[:next_step],
|
|
89
|
+
focus_step: scoped[:focus_step],
|
|
90
|
+
scope_root: scoped[:root]
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def scoped_status_view(state, scope)
|
|
95
|
+
if scope.nil? || scope.strip.empty?
|
|
96
|
+
active_steps = state.active_steps
|
|
97
|
+
next_step = active_steps.empty? ? state.next_workable : nil
|
|
98
|
+
return {state: state, active_steps: active_steps, next_step: next_step, focus_step: state.current || next_step, root: nil}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
root = state.find_by_number(scope.strip)
|
|
102
|
+
raise StepErrors::NotFound, "Step #{scope} not found in queue" unless root
|
|
103
|
+
|
|
104
|
+
scoped_steps = state.subtree_steps(root.number)
|
|
105
|
+
scoped_state = Models::QueueState.new(steps: scoped_steps, assignment: state.assignment)
|
|
106
|
+
active_steps = scoped_state.active_steps
|
|
107
|
+
next_step = active_steps.empty? ? state.next_workable_in_subtree(root.number) : nil
|
|
108
|
+
|
|
109
|
+
{state: scoped_state, active_steps: active_steps, next_step: next_step, focus_step: scoped_state.current || next_step, root: root.number}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fork_scope_root(state, step)
|
|
113
|
+
return nil unless step
|
|
114
|
+
return step if step.fork?
|
|
115
|
+
|
|
116
|
+
state.nearest_fork_ancestor(step.number)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def scoped_fork_metadata_step(state, step, scope, scope_root)
|
|
120
|
+
return nil unless step
|
|
121
|
+
|
|
122
|
+
if scope && !scope.strip.empty?
|
|
123
|
+
return state.find_by_number(scope_root || scope.strip)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
fork_scope_root(state, step)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def effective_fork_provider_for(step, scoped_fork_step)
|
|
130
|
+
return nil unless step
|
|
131
|
+
|
|
132
|
+
provider = step.fork_provider || scoped_fork_step&.fork_provider
|
|
133
|
+
provider.to_s.strip.empty? ? nil : provider
|
|
134
|
+
end
|
|
51
135
|
end
|
|
52
136
|
end
|
|
53
137
|
end
|
|
@@ -32,7 +32,7 @@ module Ace
|
|
|
32
32
|
unless options[:quiet]
|
|
33
33
|
print_terminal_skip_summary(result[:skipped_terminal])
|
|
34
34
|
print_assignment_header(result[:assignment])
|
|
35
|
-
print_step_instructions(result[:current])
|
|
35
|
+
print_step_instructions(result[:current] || result[:state]&.next_workable)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -21,7 +21,7 @@ module Ace
|
|
|
21
21
|
|
|
22
22
|
target = resolve_assignment_target(options)
|
|
23
23
|
executor = build_executor_for_target(target)
|
|
24
|
-
result = executor.fail(message)
|
|
24
|
+
result = executor.fail(message, fork_root: target.scope)
|
|
25
25
|
|
|
26
26
|
unless options[:quiet]
|
|
27
27
|
failed = result[:failed]
|
|
@@ -4,12 +4,12 @@ module Ace
|
|
|
4
4
|
module Assign
|
|
5
5
|
module CLI
|
|
6
6
|
module Commands
|
|
7
|
-
# Complete
|
|
7
|
+
# Complete active step with report content
|
|
8
8
|
class Finish < Ace::Support::Cli::Command
|
|
9
9
|
include Ace::Support::Cli::Base
|
|
10
10
|
include AssignmentTarget
|
|
11
11
|
|
|
12
|
-
desc "Complete
|
|
12
|
+
desc "Complete active step with report content"
|
|
13
13
|
|
|
14
14
|
argument :step, required: false, desc: "Step number to finish (active assignment only)"
|
|
15
15
|
option :message, aliases: ["-m"], desc: "Report content: string, file path, or pipe stdin"
|
|
@@ -41,13 +41,24 @@ module Ace
|
|
|
41
41
|
report_path = File.join(assignment.reports_dir, report_filename)
|
|
42
42
|
puts "Report saved to: #{report_path}"
|
|
43
43
|
|
|
44
|
-
if
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
scoped_state = if target.scope && !target.scope.to_s.strip.empty?
|
|
45
|
+
Ace::Assign::Models::QueueState.new(
|
|
46
|
+
steps: result[:state].subtree_steps(target.scope.strip),
|
|
47
|
+
assignment: assignment
|
|
48
|
+
)
|
|
49
|
+
else
|
|
50
|
+
result[:state]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
active_steps = scoped_state.active_steps
|
|
54
|
+
if active_steps.any?
|
|
55
|
+
focused = scoped_state.current
|
|
56
|
+
puts "Active steps remaining: #{active_steps.map { |step| "#{step.number} #{step.name}" }.join(', ')}"
|
|
57
|
+
puts "Next: ace-assign step#{step_target_suffix(focused.number, options[:assignment])}" if focused
|
|
58
|
+
elsif (next_step = next_pending_step(result[:state], scoped_state, target.scope))
|
|
59
|
+
puts "No active step selected."
|
|
60
|
+
puts "Next pending step: #{next_step.number} - #{next_step.name}"
|
|
49
61
|
else
|
|
50
|
-
puts
|
|
51
62
|
fork_root = target.scope&.strip
|
|
52
63
|
if fork_root && result[:state].subtree_complete?(fork_root)
|
|
53
64
|
puts "Fork subtree #{fork_root} completed."
|
|
@@ -81,6 +92,19 @@ module Ace
|
|
|
81
92
|
rescue IOError, Errno::EBADF
|
|
82
93
|
nil
|
|
83
94
|
end
|
|
95
|
+
|
|
96
|
+
def step_target_suffix(step_number, assignment_target)
|
|
97
|
+
return " #{step_number}" if assignment_target.nil? || assignment_target.to_s.strip.empty?
|
|
98
|
+
|
|
99
|
+
%( #{step_number} --assignment "#{assignment_target}")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def next_pending_step(state, scoped_state, scope_root)
|
|
103
|
+
scope_ref = scope_root.to_s.strip
|
|
104
|
+
return scoped_state.next_workable if scope_ref.empty?
|
|
105
|
+
|
|
106
|
+
state.next_workable_in_subtree(scope_ref)
|
|
107
|
+
end
|
|
84
108
|
end
|
|
85
109
|
end
|
|
86
110
|
end
|