ace-assign 0.40.4 → 0.41.4
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/steps/squash-changelog.step.yml +1 -0
- data/CHANGELOG.md +46 -1
- data/README.md +1 -1
- data/docs/demo/fork-provider.tape.yml +34 -0
- data/docs/usage.md +19 -0
- data/handbook/guides/fork-context.g.md +18 -1
- data/lib/ace/assign/atoms/step_file_parser.rb +11 -0
- data/lib/ace/assign/cli/commands/fork_run.rb +10 -2
- data/lib/ace/assign/cli/commands/status.rb +17 -4
- data/lib/ace/assign/models/step.rb +34 -1
- data/lib/ace/assign/molecules/queue_scanner.rb +1 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +25 -1
- data/lib/ace/assign/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fe983fae430fd751bcb94ffb6138e7ca3abc91bbde8daf00f7de512a5eaa65a
|
|
4
|
+
data.tar.gz: 156f73f37951578ea97aa0a6bd0b7622b3f9a4e71f6fa9aea5b4f60f21ed4623
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e6306caa8bfc6218caeb776278b384c9db47751273ea0ff0713ca05d25da18a52c31410eab38a2f82777e26b6771927bb5c6919da62f9d870cfcc972e04323c
|
|
7
|
+
data.tar.gz: 6245400297bf3c41329c5abaee36a57d440c4ae90a042d3134051120956affca84a3b00b9ba653ef313c4a0f034209185f26ea860ab9f7ca8848068d0ea12182
|
|
@@ -23,6 +23,7 @@ context:
|
|
|
23
23
|
when_to_skip:
|
|
24
24
|
- "There is only one relevant changelog entry"
|
|
25
25
|
- "Changelog has already been consolidated for this branch"
|
|
26
|
+
- "Root changelog entries are under [Unreleased] (no versioned entries to squash)"
|
|
26
27
|
|
|
27
28
|
effort: trivial
|
|
28
29
|
tags: [docs, changelog, release]
|
data/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.41.4] - 2026-03-29
|
|
11
|
+
|
|
12
|
+
### Technical
|
|
13
|
+
- Added `when_to_skip` entry for unreleased-mode branches to the `squash-changelog` catalog step.
|
|
14
|
+
|
|
15
|
+
## [0.41.3] - 2026-03-29
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Reused one resolved provider value in `ace-assign fork-run` for both runtime display and launcher execution flow.
|
|
19
|
+
|
|
20
|
+
### Technical
|
|
21
|
+
- Clarified inline documentation of intentional fork-context merge semantics in `AssignmentExecutor` (`=` overwrite for generated children, `||=` default-preserve for materialized explicit steps).
|
|
22
|
+
|
|
23
|
+
## [0.41.2] - 2026-03-29
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Updated scoped `ace-assign status` output (text and JSON `current_step`) to show the effective fork provider inherited from the scoped fork root when the active child has no local provider.
|
|
27
|
+
- Removed unreachable symbol-key provider fallback in `Step#fork_provider` now that fork options are normalized to string keys.
|
|
28
|
+
|
|
29
|
+
### Technical
|
|
30
|
+
- Added scoped status regression tests covering fork-provider display/serialization for fork-root scope (`@010`) with active child steps.
|
|
31
|
+
- Documented intentional fork-context propagation semantics in `AssignmentExecutor` (generated child-step overwrite vs explicit-step default-preserve behavior).
|
|
32
|
+
|
|
33
|
+
## [0.41.1] - 2026-03-29
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Preserved catalog `context.fork` provider overrides for explicit `skill`-backed step materialization so step-level fork provider precedence is applied correctly.
|
|
37
|
+
- Kept fork child-step behavior stable by avoiding implicit fork-context propagation from canonical metadata when children do not explicitly declare fork context.
|
|
38
|
+
|
|
39
|
+
### Technical
|
|
40
|
+
- Added regression coverage for explicit `skill` and explicit `workflow` materialization paths to ensure catalog fork overrides are retained.
|
|
41
|
+
|
|
42
|
+
## [0.41.0] - 2026-03-28
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- Added per-step fork provider overrides via step frontmatter `fork.provider`.
|
|
46
|
+
- Added `fork_provider` exposure in `ace-assign status` current-step output and `--format json` payloads.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
- Updated `ace-assign fork-run` provider precedence to: CLI `--provider` > step `fork.provider` > config `execution.provider` > default.
|
|
50
|
+
- Extended skill/catalog step materialization to carry `context.fork` options into generated step metadata.
|
|
51
|
+
|
|
52
|
+
### Technical
|
|
53
|
+
- Added coverage for step model/parser/scanner fork-option round-trip, fork-run provider precedence, status serialization/display, and executor composition behavior.
|
|
54
|
+
- Updated usage and fork-context docs with `fork.provider` examples and precedence rules.
|
|
10
55
|
|
|
11
56
|
## [0.40.4] - 2026-03-29
|
|
12
57
|
|
|
13
58
|
### Fixed
|
|
14
|
-
-
|
|
59
|
+
- Bumped dependency constraints to currently available `~>` ranges on RubyGems and updated release metadata after dependency synchronization.
|
|
15
60
|
|
|
16
61
|
## [0.40.3] - 2026-03-27
|
|
17
62
|
|
data/README.md
CHANGED
|
@@ -78,7 +78,7 @@ The easiest way to start is through [ace-overseer](../ace-overseer) -- define a
|
|
|
78
78
|
|
|
79
79
|
**Define assignments from presets** - pick a [preset](.ace-defaults/assign/presets/) like [`work-on-task`](.ace-defaults/assign/presets/work-on-task.yml) or `release-only`, pass parameters (task refs, packages), and run `ace-assign create --task ...` or [`ace-assign create --yaml ...`](docs/usage.md) to expand them into a concrete step queue. Steps are defined in the [catalog](.ace-defaults/assign/catalog/steps/) (e.g., [`work-on-task.step.yml`](.ace-defaults/assign/catalog/steps/work-on-task.step.yml)) and ordered by [composition rules](.ace-defaults/assign/catalog/composition-rules.yml). Compose custom assignments with `/as-assign-compose`.
|
|
80
80
|
|
|
81
|
-
**Run with orchestrator and fork agents** - use `/as-assign-drive` to walk through steps, forking long-running work (implementation, review, release) to isolated agent subprocesses with configurable [execution defaults](.ace-defaults/assign/config.yml). Forks can run sequentially or as parallel batches, each producing inspectable traces and session reports under `.ace-local/assign/`.
|
|
81
|
+
**Run with orchestrator and fork agents** - use `/as-assign-drive` to walk through steps, forking long-running work (implementation, review, release) to isolated agent subprocesses with configurable [execution defaults](.ace-defaults/assign/config.yml) or per-step `fork.provider` overrides. Forks can run sequentially or as parallel batches, each producing inspectable traces and session reports under `.ace-local/assign/`.
|
|
82
82
|
|
|
83
83
|
**Recover from failure without losing history** - keep failed-step lineage intact, inject targeted retries or fix steps, and continue execution with auditable failure evidence.
|
|
84
84
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show per-step fork provider — different steps run with different LLM models
|
|
3
|
+
tags:
|
|
4
|
+
- ace-assign
|
|
5
|
+
- fork
|
|
6
|
+
- provider
|
|
7
|
+
settings:
|
|
8
|
+
font_size: 16
|
|
9
|
+
width: 1000
|
|
10
|
+
height: 500
|
|
11
|
+
format: gif
|
|
12
|
+
setup:
|
|
13
|
+
- sandbox
|
|
14
|
+
- git-init
|
|
15
|
+
- copy-fixtures
|
|
16
|
+
scenes:
|
|
17
|
+
- name: Show step frontmatter with fork.provider
|
|
18
|
+
commands:
|
|
19
|
+
- type: "cat .ace-local/assign/demoXx/steps/020-implement.st.md"
|
|
20
|
+
sleep: 4s
|
|
21
|
+
- name: Status shows per-step Fork Provider
|
|
22
|
+
commands:
|
|
23
|
+
- type: clear
|
|
24
|
+
sleep: 1s
|
|
25
|
+
- type: "ace-assign status"
|
|
26
|
+
sleep: 4s
|
|
27
|
+
- name: fork-run resolves step provider
|
|
28
|
+
commands:
|
|
29
|
+
- type: clear
|
|
30
|
+
sleep: 1s
|
|
31
|
+
- type: "ace-assign fork-run --root 020 --dry-run"
|
|
32
|
+
sleep: 4s
|
|
33
|
+
teardown:
|
|
34
|
+
- cleanup
|
data/docs/usage.md
CHANGED
|
@@ -171,6 +171,25 @@ Options:
|
|
|
171
171
|
- `--quiet, -q`
|
|
172
172
|
- `--debug, -d`
|
|
173
173
|
|
|
174
|
+
Provider resolution precedence for fork execution:
|
|
175
|
+
|
|
176
|
+
1. CLI `--provider`
|
|
177
|
+
2. Step frontmatter `fork.provider`
|
|
178
|
+
3. Config `execution.provider`
|
|
179
|
+
4. Built-in default provider
|
|
180
|
+
|
|
181
|
+
Step-level example:
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
---
|
|
185
|
+
name: research
|
|
186
|
+
status: pending
|
|
187
|
+
context: fork
|
|
188
|
+
fork:
|
|
189
|
+
provider: "claude:sonnet@yolo"
|
|
190
|
+
---
|
|
191
|
+
```
|
|
192
|
+
|
|
174
193
|
### `ace-assign list`
|
|
175
194
|
|
|
176
195
|
List assignments.
|
|
@@ -13,6 +13,23 @@ ace-docs:
|
|
|
13
13
|
|
|
14
14
|
Fork context enables step files to run in isolated agent contexts using the Task tool. When a step has `context: fork` in its frontmatter, ace-assign outputs instructions for the orchestrating agent to execute the step via a subagent.
|
|
15
15
|
|
|
16
|
+
You can also set a per-step provider override with `fork.provider`:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
---
|
|
20
|
+
context: fork
|
|
21
|
+
fork:
|
|
22
|
+
provider: "claude:sonnet@yolo"
|
|
23
|
+
---
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Provider precedence during `ace-assign fork-run`:
|
|
27
|
+
|
|
28
|
+
1. CLI `--provider`
|
|
29
|
+
2. Step `fork.provider`
|
|
30
|
+
3. Assign config `execution.provider`
|
|
31
|
+
4. Built-in default
|
|
32
|
+
|
|
16
33
|
For hierarchical split workflows, use **parent-only** fork markers:
|
|
17
34
|
- Split parent step: `context: fork`
|
|
18
35
|
- Child steps (`onboard-base`, `task-load`, `plan-task`, `work-on-task`, `verify-test`, `release-minor`): no `context: fork`
|
|
@@ -228,4 +245,4 @@ ACE_DEBUG=1 ace-assign status
|
|
|
228
245
|
|
|
229
246
|
- [ace-assign README](../../README.md) - Main documentation
|
|
230
247
|
- [Work Queue Model](../workflow-instructions/drive-assignment.wf.md) - Assignment management
|
|
231
|
-
- `ace-assign fork-run --root <step> --assignment <id>` - Prepare subtree-scoped fork session
|
|
248
|
+
- `ace-assign fork-run --root <step> --assignment <id>` - Prepare subtree-scoped fork session
|
|
@@ -56,6 +56,7 @@ module Ace
|
|
|
56
56
|
parallel: parse_boolean(fm["parallel"]),
|
|
57
57
|
max_parallel: parse_positive_integer(fm["max_parallel"]),
|
|
58
58
|
fork_retry_limit: parse_non_negative_integer(fm["fork_retry_limit"]),
|
|
59
|
+
fork_options: parse_hash(fm["fork"]),
|
|
59
60
|
started_at: parse_time(fm["started_at"]),
|
|
60
61
|
completed_at: parse_time(fm["completed_at"]),
|
|
61
62
|
fork_launch_pid: parse_integer(fm["fork_launch_pid"]),
|
|
@@ -174,6 +175,16 @@ module Ace
|
|
|
174
175
|
end
|
|
175
176
|
private_class_method :parse_integer_array
|
|
176
177
|
|
|
178
|
+
def self.parse_hash(value)
|
|
179
|
+
return nil unless value.is_a?(Hash)
|
|
180
|
+
|
|
181
|
+
normalized = value.each_with_object({}) do |(key, val), memo|
|
|
182
|
+
memo[key.to_s] = val
|
|
183
|
+
end
|
|
184
|
+
normalized.empty? ? nil : normalized
|
|
185
|
+
end
|
|
186
|
+
private_class_method :parse_hash
|
|
187
|
+
|
|
177
188
|
def self.parse_boolean(value)
|
|
178
189
|
return nil if value.nil?
|
|
179
190
|
return value if value == true || value == false
|
|
@@ -39,6 +39,7 @@ module Ace
|
|
|
39
39
|
|
|
40
40
|
root_step = resolve_root_step(state, current, options[:root], target.scope)
|
|
41
41
|
ensure_root_is_fork!(root_step)
|
|
42
|
+
resolved_provider = resolved_provider_for(root_step, options[:provider])
|
|
42
43
|
|
|
43
44
|
if state.subtree_complete?(root_step.number)
|
|
44
45
|
puts "Subtree #{root_step.number} is already complete." unless options[:quiet]
|
|
@@ -49,7 +50,7 @@ module Ace
|
|
|
49
50
|
next_step = state.next_workable_in_subtree(root_step.number)
|
|
50
51
|
puts "Starting fork subtree execution: #{root_step.number} - #{root_step.name}"
|
|
51
52
|
puts "Assignment: #{assignment.id}"
|
|
52
|
-
puts "Provider: #{
|
|
53
|
+
puts "Provider: #{resolved_provider}"
|
|
53
54
|
puts "Timeout: #{options[:timeout] || Ace::Assign.config.dig("execution", "timeout") || Molecules::ForkSessionLauncher::DEFAULT_TIMEOUT}s"
|
|
54
55
|
puts "Next step: #{next_step.number} - #{next_step.name}" if next_step
|
|
55
56
|
end
|
|
@@ -73,7 +74,7 @@ module Ace
|
|
|
73
74
|
launch_result = launcher.launch(
|
|
74
75
|
assignment_id: assignment.id,
|
|
75
76
|
fork_root: root_step.number,
|
|
76
|
-
provider:
|
|
77
|
+
provider: resolved_provider,
|
|
77
78
|
cli_args: options[:cli_args],
|
|
78
79
|
timeout: options[:timeout],
|
|
79
80
|
cache_dir: assignment.cache_dir
|
|
@@ -186,6 +187,13 @@ module Ace
|
|
|
186
187
|
raise Error, "Step #{root_step.number} is not fork-enabled (context: fork missing)."
|
|
187
188
|
end
|
|
188
189
|
|
|
190
|
+
def resolved_provider_for(root_step, cli_provider)
|
|
191
|
+
explicit = cli_provider&.to_s&.strip
|
|
192
|
+
return explicit unless explicit.nil? || explicit.empty?
|
|
193
|
+
|
|
194
|
+
root_step.fork_provider || Ace::Assign.config.dig("execution", "provider") || Molecules::ForkSessionLauncher::DEFAULT_PROVIDER
|
|
195
|
+
end
|
|
196
|
+
|
|
189
197
|
def record_fork_pid_info(root_step, launch_result)
|
|
190
198
|
pid_info = launch_result.is_a?(Hash) ? launch_result[:fork_pid_info] : nil
|
|
191
199
|
return unless pid_info
|
|
@@ -72,7 +72,8 @@ module Ace
|
|
|
72
72
|
|
|
73
73
|
unless options[:quiet]
|
|
74
74
|
if options[:format] == "json"
|
|
75
|
-
|
|
75
|
+
scoped_fork_step = scoped_fork_metadata_step(state, current_for_display, target.scope, scope_root)
|
|
76
|
+
puts JSON.pretty_generate(status_to_h(assignment, scoped_state, current_for_display, scoped_fork_step: scoped_fork_step))
|
|
76
77
|
return
|
|
77
78
|
end
|
|
78
79
|
|
|
@@ -98,6 +99,10 @@ module Ace
|
|
|
98
99
|
if current_for_display.context
|
|
99
100
|
puts "Context: #{current_for_display.context}"
|
|
100
101
|
end
|
|
102
|
+
effective_fork_provider = effective_fork_provider_for(current_for_display, scoped_fork_step)
|
|
103
|
+
if effective_fork_provider
|
|
104
|
+
puts "Fork Provider: #{effective_fork_provider}"
|
|
105
|
+
end
|
|
101
106
|
puts
|
|
102
107
|
print_scoped_fork_pid_info(scoped_fork_step)
|
|
103
108
|
|
|
@@ -129,7 +134,7 @@ module Ace
|
|
|
129
134
|
|
|
130
135
|
private
|
|
131
136
|
|
|
132
|
-
def status_to_h(assignment, state, current_step)
|
|
137
|
+
def status_to_h(assignment, state, current_step, scoped_fork_step: nil)
|
|
133
138
|
{
|
|
134
139
|
assignment: {
|
|
135
140
|
id: assignment.id,
|
|
@@ -137,12 +142,12 @@ module Ace
|
|
|
137
142
|
state: state.assignment_state.to_s
|
|
138
143
|
},
|
|
139
144
|
steps: state.steps.map { |step| step_to_h(step) },
|
|
140
|
-
current_step: step_to_h(current_step),
|
|
145
|
+
current_step: step_to_h(current_step, effective_fork_provider: effective_fork_provider_for(current_step, scoped_fork_step)),
|
|
141
146
|
progress: "#{state.done.size}/#{state.size} done"
|
|
142
147
|
}
|
|
143
148
|
end
|
|
144
149
|
|
|
145
|
-
def step_to_h(step)
|
|
150
|
+
def step_to_h(step, effective_fork_provider: nil)
|
|
146
151
|
return nil unless step
|
|
147
152
|
|
|
148
153
|
{
|
|
@@ -152,6 +157,7 @@ module Ace
|
|
|
152
157
|
skill: step.skill,
|
|
153
158
|
workflow: step.workflow,
|
|
154
159
|
context: step.context,
|
|
160
|
+
fork_provider: effective_fork_provider || step.fork_provider,
|
|
155
161
|
batch_parent: step.batch_parent,
|
|
156
162
|
parallel: step.parallel,
|
|
157
163
|
max_parallel: step.max_parallel,
|
|
@@ -342,6 +348,13 @@ module Ace
|
|
|
342
348
|
fork_scope_root(state, current_step)
|
|
343
349
|
end
|
|
344
350
|
|
|
351
|
+
def effective_fork_provider_for(current_step, scoped_fork_step)
|
|
352
|
+
return nil unless current_step
|
|
353
|
+
|
|
354
|
+
provider = current_step.fork_provider || scoped_fork_step&.fork_provider
|
|
355
|
+
provider.to_s.strip.empty? ? nil : provider
|
|
356
|
+
end
|
|
357
|
+
|
|
345
358
|
def print_scoped_fork_pid_info(step)
|
|
346
359
|
return unless step
|
|
347
360
|
|
|
@@ -25,7 +25,7 @@ module Ace
|
|
|
25
25
|
attr_reader :number, :name, :status, :instructions, :report, :error,
|
|
26
26
|
:started_at, :completed_at, :added_by, :parent, :file_path, :skill, :context,
|
|
27
27
|
:workflow,
|
|
28
|
-
:batch_parent, :parallel, :max_parallel, :fork_retry_limit,
|
|
28
|
+
:batch_parent, :parallel, :max_parallel, :fork_retry_limit, :fork_options,
|
|
29
29
|
:fork_launch_pid, :fork_tracked_pids, :fork_pid_updated_at, :fork_pid_file,
|
|
30
30
|
:stall_reason
|
|
31
31
|
|
|
@@ -46,6 +46,7 @@ module Ace
|
|
|
46
46
|
# @param parallel [Boolean, nil] Batch scheduling mode hint (true=parallel, false=sequential)
|
|
47
47
|
# @param max_parallel [Integer, nil] Max concurrent children for parallel batches
|
|
48
48
|
# @param fork_retry_limit [Integer, nil] Retry attempts allowed per failed child
|
|
49
|
+
# @param fork_options [Hash, nil] Fork execution options from frontmatter (e.g. {provider: "..."} )
|
|
49
50
|
# @param fork_launch_pid [Integer, nil] PID of the process that launched the fork run
|
|
50
51
|
# @param fork_tracked_pids [Array<Integer>, nil] Observed subprocess/descendant PIDs during fork execution
|
|
51
52
|
# @param fork_pid_updated_at [Time, nil] Timestamp when fork PID metadata was last updated
|
|
@@ -55,6 +56,7 @@ module Ace
|
|
|
55
56
|
started_at: nil, completed_at: nil, added_by: nil, parent: nil,
|
|
56
57
|
file_path: nil, skill: nil, workflow: nil, context: nil,
|
|
57
58
|
batch_parent: nil, parallel: nil, max_parallel: nil, fork_retry_limit: nil,
|
|
59
|
+
fork_options: nil,
|
|
58
60
|
fork_launch_pid: nil, fork_tracked_pids: nil, fork_pid_updated_at: nil,
|
|
59
61
|
fork_pid_file: nil, stall_reason: nil)
|
|
60
62
|
validate_status!(status)
|
|
@@ -63,6 +65,7 @@ module Ace
|
|
|
63
65
|
validate_boolean!(:parallel, parallel)
|
|
64
66
|
validate_positive_integer!(:max_parallel, max_parallel)
|
|
65
67
|
validate_non_negative_integer!(:fork_retry_limit, fork_retry_limit)
|
|
68
|
+
validate_hash!(:fork_options, fork_options)
|
|
66
69
|
|
|
67
70
|
@number = number.freeze
|
|
68
71
|
@name = name.freeze
|
|
@@ -82,6 +85,7 @@ module Ace
|
|
|
82
85
|
@parallel = parallel.nil? ? nil : !!parallel
|
|
83
86
|
@max_parallel = max_parallel&.to_i
|
|
84
87
|
@fork_retry_limit = fork_retry_limit&.to_i
|
|
88
|
+
@fork_options = normalize_fork_options(fork_options)
|
|
85
89
|
@fork_launch_pid = fork_launch_pid&.to_i
|
|
86
90
|
@fork_tracked_pids = Array(fork_tracked_pids).map(&:to_i).uniq.sort.freeze
|
|
87
91
|
@fork_pid_updated_at = fork_pid_updated_at
|
|
@@ -113,6 +117,16 @@ module Ace
|
|
|
113
117
|
context == "fork"
|
|
114
118
|
end
|
|
115
119
|
|
|
120
|
+
# Resolve per-step provider override from fork options.
|
|
121
|
+
# @return [String, nil]
|
|
122
|
+
def fork_provider
|
|
123
|
+
return nil unless fork_options
|
|
124
|
+
|
|
125
|
+
provider = fork_options["provider"]
|
|
126
|
+
provider = provider.to_s.strip
|
|
127
|
+
provider.empty? ? nil : provider
|
|
128
|
+
end
|
|
129
|
+
|
|
116
130
|
# Get the original step number if this is a retry
|
|
117
131
|
# @return [String, nil] Original step number
|
|
118
132
|
def retry_of
|
|
@@ -134,6 +148,7 @@ module Ace
|
|
|
134
148
|
"parallel" => parallel,
|
|
135
149
|
"max_parallel" => max_parallel,
|
|
136
150
|
"fork_retry_limit" => fork_retry_limit,
|
|
151
|
+
"fork" => fork_options,
|
|
137
152
|
"started_at" => started_at&.iso8601,
|
|
138
153
|
"completed_at" => completed_at&.iso8601,
|
|
139
154
|
"fork_launch_pid" => fork_launch_pid,
|
|
@@ -191,6 +206,24 @@ module Ace
|
|
|
191
206
|
|
|
192
207
|
raise ArgumentError, "Invalid #{field_name}: #{value.inspect}. Must be an integer >= 0"
|
|
193
208
|
end
|
|
209
|
+
|
|
210
|
+
def validate_hash!(field_name, value)
|
|
211
|
+
return if value.nil? || value.is_a?(Hash)
|
|
212
|
+
|
|
213
|
+
raise ArgumentError, "Invalid #{field_name}: #{value.inspect}. Must be a Hash or nil"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def normalize_fork_options(value)
|
|
217
|
+
return nil unless value.is_a?(Hash)
|
|
218
|
+
|
|
219
|
+
normalized = value.each_with_object({}) do |(key, val), memo|
|
|
220
|
+
memo[key.to_s] = val
|
|
221
|
+
end
|
|
222
|
+
normalized.compact!
|
|
223
|
+
return nil if normalized.empty?
|
|
224
|
+
|
|
225
|
+
normalized.freeze
|
|
226
|
+
end
|
|
194
227
|
end
|
|
195
228
|
end
|
|
196
229
|
end
|
|
@@ -706,6 +706,14 @@ module Ace
|
|
|
706
706
|
|
|
707
707
|
context_default = step_def.dig("context", "default")
|
|
708
708
|
child["context"] = context_default if context_default && parent_context != "fork"
|
|
709
|
+
fork_context = step_def.dig("context", "fork")
|
|
710
|
+
if child["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
|
|
711
|
+
# Generated child sub-steps have no explicit frontmatter overrides.
|
|
712
|
+
# Apply the catalog fork context directly (overwrite semantics) so
|
|
713
|
+
# delegated children inherit the scheduler/provider policy configured
|
|
714
|
+
# for that child step type.
|
|
715
|
+
child["fork"] = fork_context
|
|
716
|
+
end
|
|
709
717
|
end
|
|
710
718
|
|
|
711
719
|
child
|
|
@@ -844,6 +852,15 @@ module Ace
|
|
|
844
852
|
"instructions" => rendered_instructions,
|
|
845
853
|
"workflow" => rendering["workflow"]
|
|
846
854
|
)
|
|
855
|
+
context_default = rendering.dig("context", "default")
|
|
856
|
+
materialized["context"] ||= context_default if context_default
|
|
857
|
+
fork_context = rendering.dig("context", "fork")
|
|
858
|
+
if materialized["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
|
|
859
|
+
# For materialized explicit steps, preserve frontmatter-provided fork config
|
|
860
|
+
# (`||=` semantics). Rendering contributes defaults only when the step
|
|
861
|
+
# itself did not declare fork options.
|
|
862
|
+
materialized["fork"] ||= fork_context
|
|
863
|
+
end
|
|
847
864
|
materialized["source_skill"] = rendering["source_skill"] || rendering["skill"] if rendering["source_skill"] || rendering["skill"]
|
|
848
865
|
materialized["source_workflow"] = rendering["workflow"] if rendering["workflow"] && !rendering["workflow"].empty?
|
|
849
866
|
materialized.delete("skill")
|
|
@@ -866,7 +883,14 @@ module Ace
|
|
|
866
883
|
|
|
867
884
|
explicit_skill = step["skill"]&.to_s&.strip
|
|
868
885
|
if explicit_skill && !explicit_skill.empty?
|
|
869
|
-
|
|
886
|
+
canonical_step = find_step_definition(step["name"]&.to_s)
|
|
887
|
+
if canonical_step && step["parent"] && !step.key?("context") && !step.key?("fork")
|
|
888
|
+
canonical_step = canonical_step.dup
|
|
889
|
+
canonical_step.delete("context")
|
|
890
|
+
canonical_step.delete("fork")
|
|
891
|
+
end
|
|
892
|
+
rendering = skill_source_resolver.resolve_skill_rendering(explicit_skill)
|
|
893
|
+
return canonical_step ? canonical_step.merge(rendering || {}) : rendering if rendering
|
|
870
894
|
end
|
|
871
895
|
|
|
872
896
|
skill_source_resolver.resolve_step_rendering(step["name"]&.to_s)
|
data/lib/ace/assign/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-assign
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.41.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
@@ -221,6 +221,7 @@ files:
|
|
|
221
221
|
- CHANGELOG.md
|
|
222
222
|
- README.md
|
|
223
223
|
- Rakefile
|
|
224
|
+
- docs/demo/fork-provider.tape.yml
|
|
224
225
|
- docs/exit-codes.md
|
|
225
226
|
- docs/getting-started.md
|
|
226
227
|
- docs/handbook.md
|