ace-assign 0.41.10 → 0.42.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/plan-task.step.yml +0 -4
- data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -4
- data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -4
- data/CHANGELOG.md +59 -0
- data/docs/usage.md +16 -3
- data/handbook/workflow-instructions/assign/drive.wf.md +42 -1
- data/lib/ace/assign/atoms/preset_expander.rb +12 -1
- data/lib/ace/assign/cli/commands/status.rb +24 -0
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +29 -3
- data/lib/ace/assign/organisms/assignment_executor.rb +71 -16
- data/lib/ace/assign/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 214f383018bb2b9861e03cedf52c8eca4df627d9787357d00998725be36cbe36
|
|
4
|
+
data.tar.gz: 5f8d577f6f0eacd399c2a317d8dee720f237194af70968b65fcca8a1de676dea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3be4c93c1264e83f808bd0fed4d92780a65e00da59561c428bf3d7a6f65ce63c21398d256113d601e3849ba25dc546c1eb841e6dbf9db21804809c3afac35239
|
|
7
|
+
data.tar.gz: daab1a233b937ef79fa3d3b406404350b6b34ca0ade254cb7ff96b6f11c810f0c7dc255fe271e8e00cd8c194c6a941330ed8af725e22d49dfcceb0c627ad1ddf
|
|
@@ -5,10 +5,6 @@ description: Analyze task requirements and create an implementation plan
|
|
|
5
5
|
produces: [implementation-plan]
|
|
6
6
|
consumes: [project-base-context, task-spec]
|
|
7
7
|
|
|
8
|
-
context:
|
|
9
|
-
default: fork
|
|
10
|
-
reason: "Planning benefits from focused exploration"
|
|
11
|
-
|
|
12
8
|
when_to_skip:
|
|
13
9
|
- "Task is trivial and doesn't need a plan"
|
|
14
10
|
- "Implementation plan already exists"
|
|
@@ -10,10 +10,6 @@ prerequisites:
|
|
|
10
10
|
produces: [review-feedback]
|
|
11
11
|
consumes: [pull-request]
|
|
12
12
|
|
|
13
|
-
context:
|
|
14
|
-
default: fork
|
|
15
|
-
reason: "Review benefits from isolated analysis without implementation bias"
|
|
16
|
-
|
|
17
13
|
when_to_skip:
|
|
18
14
|
- "No code changes since last review"
|
|
19
15
|
- "Changes are trivial (typo fix, config update)"
|
|
@@ -11,10 +11,6 @@ intent:
|
|
|
11
11
|
produces: [code-changes, commits]
|
|
12
12
|
consumes: [project-base-context, task-spec]
|
|
13
13
|
|
|
14
|
-
context:
|
|
15
|
-
default: fork
|
|
16
|
-
reason: "Substantial implementation benefits from focused agent"
|
|
17
|
-
|
|
18
14
|
when_to_skip:
|
|
19
15
|
- "Task is documentation-only"
|
|
20
16
|
- "Changes are already implemented"
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.42.4] - 2026-04-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Scoped canonical `skill://` and `wfi://` source discovery to in-project defaults and explicitly registered external sources so ambient installed gems no longer leak into assign resolution.
|
|
14
|
+
- Preserved child `skill` metadata only for hand-authored explicit split sub-steps while keeping inferred and preset-expanded canonical sub-steps fully materialized.
|
|
15
|
+
|
|
16
|
+
## [0.42.3] - 2026-04-02
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Updated HITL guidance wording in `ace-assign status` and assignment workflow/docs to use canonical "event" terminology (`Review event`, `HITL event`).
|
|
20
|
+
|
|
21
|
+
### Technical
|
|
22
|
+
- Refreshed status command test expectations for the updated HITL wording contract.
|
|
23
|
+
|
|
24
|
+
## [0.42.2] - 2026-04-02
|
|
25
|
+
|
|
26
|
+
### Technical
|
|
27
|
+
- Updated HITL stall-path fixture expectations in status command coverage from `.ace-hitl/...` to `.ace-local/hitl/...` to match the new default HITL root.
|
|
28
|
+
|
|
29
|
+
## [0.42.1] - 2026-04-02
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Updated `wfi://assign/drive` HITL guidance to use per-item polling (`ace-hitl wait <id>`) as the default requester path and `ace-hitl update --resume` as fallback dispatch.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Updated `ace-assign status` HITL operator guidance output to display polling-first and fallback resume commands aligned with the current HITL contract.
|
|
36
|
+
|
|
37
|
+
### Technical
|
|
38
|
+
- Refreshed command-level status tests for the new HITL guidance text contract.
|
|
39
|
+
|
|
40
|
+
## [0.42.0] - 2026-04-01
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Added a dedicated HITL stall protocol to `wfi://assign/drive`, documenting `ace-hitl` create/list/show/update usage, canonical `ace-assign fail --message "HITL: <id> <path>"` formatting, and resume/archive flow without reintroducing gate-state mechanics.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- `ace-assign status` now detects HITL-formatted stall reasons and prints direct operator guidance for `ace-hitl show <id>` plus stored-path hints.
|
|
47
|
+
|
|
48
|
+
### Technical
|
|
49
|
+
- Added command-level status coverage for HITL and non-HITL stall reason rendering to prevent regressions in current-step guidance output.
|
|
50
|
+
|
|
51
|
+
## [0.41.12] - 2026-04-01
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- Restored parent-only fork boundaries for generated split subtrees by preventing child steps from inheriting fork context defaults when the parent step is the fork root.
|
|
55
|
+
- Removed default fork context declarations from canonical `plan-task`, `work-on-task`, and `review-pr` catalog steps so only explicitly fork-root parent steps run in forked context.
|
|
56
|
+
|
|
57
|
+
### Technical
|
|
58
|
+
- Added regression coverage for symbolized/string fork-context normalization in child-step materialization paths.
|
|
59
|
+
|
|
60
|
+
## [0.41.11] - 2026-04-01
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- Restored parent-only fork semantics for split-subtree execution by preventing canonical child-step `context: fork` defaults from being materialized onto generated subtree children (notably `plan-task` and `work-on-task`).
|
|
64
|
+
- Updated workflow-backed child-step rendering to strip inherited fork context/provider metadata when the child did not explicitly declare fork settings.
|
|
65
|
+
|
|
66
|
+
### Technical
|
|
67
|
+
- Added regression coverage for split-subtree child materialization and scoped status output to ensure child `plan-task` does not emit fork-execution guidance.
|
|
68
|
+
|
|
10
69
|
## [0.41.10] - 2026-03-31
|
|
11
70
|
|
|
12
71
|
### Changed
|
data/docs/usage.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
doc-type: user
|
|
3
3
|
title: ace-assign Usage Guide
|
|
4
|
-
purpose: Complete command reference for ace-assign queue orchestration, hierarchy,
|
|
4
|
+
purpose: Complete command reference for ace-assign queue orchestration, hierarchy,
|
|
5
|
+
and fork execution.
|
|
5
6
|
ace-docs:
|
|
6
|
-
last-updated: 2026-
|
|
7
|
-
last-checked: 2026-
|
|
7
|
+
last-updated: '2026-04-01'
|
|
8
|
+
last-checked: '2026-04-01'
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# ace-assign Usage Guide
|
|
@@ -92,6 +93,18 @@ Options:
|
|
|
92
93
|
- `--quiet, -q`
|
|
93
94
|
- `--debug, -d`
|
|
94
95
|
|
|
96
|
+
HITL stall behavior:
|
|
97
|
+
|
|
98
|
+
- Canonical contract lives in `wfi://hitl` (`ace-hitl` package workflow).
|
|
99
|
+
- If a step is failed with canonical message format `HITL: <id> <path>`, `ace-assign status` prints operator guidance with the matching `ace-hitl show <id>` command and available path hint.
|
|
100
|
+
- Recommended resume flow:
|
|
101
|
+
- `ace-hitl show <id>`
|
|
102
|
+
- requester path (default): `ace-hitl wait <id>`
|
|
103
|
+
- fallback path (when waiter inactive): `ace-hitl update <id> --answer "<decision>" --resume`
|
|
104
|
+
- `ace-assign retry <failed-step> --assignment <assignment-id>`
|
|
105
|
+
- Completion-attention flow:
|
|
106
|
+
- When assignment work is complete but explicit user action is needed, create an approval HITL event (`kind=approval`) and include the resume instruction for `/as-assign-drive <assignment-id>`.
|
|
107
|
+
|
|
95
108
|
### `ace-assign start [STEP]`
|
|
96
109
|
|
|
97
110
|
Start next workable pending step, or an explicit pending step in the active assignment.
|
|
@@ -437,6 +437,41 @@ For external-facing steps (for example PR/review/release/push/update lifecycle s
|
|
|
437
437
|
ace-assign fail --message "Command failed: <cmd>. Error: <exact stderr>" --assignment "$ASSIGNMENT_TARGET"
|
|
438
438
|
```
|
|
439
439
|
|
|
440
|
+
### Human-in-the-Loop (HITL) Stall Protocol
|
|
441
|
+
|
|
442
|
+
Canonical source of truth: `wfi://hitl`.
|
|
443
|
+
|
|
444
|
+
Use HITL when:
|
|
445
|
+
|
|
446
|
+
- the active step is blocked by human judgment (ambiguity, product decision, policy choice), or
|
|
447
|
+
- the assignment is complete but explicit user attention is required before next action.
|
|
448
|
+
|
|
449
|
+
For a blocked step:
|
|
450
|
+
|
|
451
|
+
1. Create a HITL event with assignment and step context:
|
|
452
|
+
```bash
|
|
453
|
+
ace-hitl create "Need product decision" --question "Should retries be visible?" --assignment <id> --step <number> --step-name <name> --resume "/as-assign-drive <id>"
|
|
454
|
+
```
|
|
455
|
+
2. Fail the step using canonical stall format:
|
|
456
|
+
```bash
|
|
457
|
+
ace-assign fail --message "HITL: <hitl-id> <hitl-path>" --assignment "$ASSIGNMENT_TARGET"
|
|
458
|
+
```
|
|
459
|
+
3. Human/operator resolves:
|
|
460
|
+
```bash
|
|
461
|
+
ace-hitl show <hitl-id>
|
|
462
|
+
ace-hitl update <hitl-id> --answer "Yes, show retries in user-facing output."
|
|
463
|
+
ace-hitl wait <hitl-id>
|
|
464
|
+
```
|
|
465
|
+
4. Discover pending HITL work:
|
|
466
|
+
- Main checkout default (smart local-first): `ace-hitl list`
|
|
467
|
+
- Explicit scope controls: `ace-hitl list --scope current` and `ace-hitl list --scope all`
|
|
468
|
+
5. Polling is default: requesting agent waits on its own HITL id (`ace-hitl wait <hitl-id>`), not global queues.
|
|
469
|
+
6. Resume dispatch is fallback: if waiter is no longer active, run:
|
|
470
|
+
```bash
|
|
471
|
+
ace-hitl update <hitl-id> --answer "<decision>" --resume
|
|
472
|
+
```
|
|
473
|
+
7. On retry/resume, read the answer from the HITL event and continue normal fail/retry mechanics. Do not introduce gate phases, assignment-level paused state, or extra resume commands in `ace-assign`.
|
|
474
|
+
|
|
440
475
|
### 5. Write Report (Only After Real Execution)
|
|
441
476
|
|
|
442
477
|
After completing the step work, write a brief report summarizing what was accomplished:
|
|
@@ -536,6 +571,12 @@ Summarize the assignment results to the user:
|
|
|
536
571
|
- Any artifacts created (PRs, commits, etc.)
|
|
537
572
|
- Next steps or follow-up actions
|
|
538
573
|
|
|
574
|
+
If completion requires explicit user action/decision, create an approval HITL event:
|
|
575
|
+
|
|
576
|
+
```bash
|
|
577
|
+
ace-hitl create "Review completed assignment results" --kind approval --question "Please confirm next action for <assignment-id>." --assignment <assignment-id> --step completion --step-name assignment-complete --resume "/as-assign-drive <assignment-id>"
|
|
578
|
+
```
|
|
579
|
+
|
|
539
580
|
## Skill Invocation Pattern
|
|
540
581
|
|
|
541
582
|
When executing a step with a `skill:` field:
|
|
@@ -663,4 +704,4 @@ $ ace-assign finish --message task-done.md --assignment "$ASSIGNMENT_TARGET"
|
|
|
663
704
|
# 7. Eventually...
|
|
664
705
|
$ ace-assign status --assignment "$ASSIGNMENT_TARGET"
|
|
665
706
|
All steps complete!
|
|
666
|
-
```
|
|
707
|
+
```
|
|
@@ -233,7 +233,8 @@ module Ace
|
|
|
233
233
|
# @param parameters [Hash] Parameter values
|
|
234
234
|
# @return [Hash] Step with substituted values
|
|
235
235
|
def self.substitute_parameters(step, parameters)
|
|
236
|
-
substitute_value(step, parameters)
|
|
236
|
+
substituted = substitute_value(step, parameters)
|
|
237
|
+
mark_preset_sub_steps_origin(substituted)
|
|
237
238
|
end
|
|
238
239
|
private_class_method :substitute_parameters
|
|
239
240
|
|
|
@@ -253,6 +254,16 @@ module Ace
|
|
|
253
254
|
end
|
|
254
255
|
private_class_method :substitute_value
|
|
255
256
|
|
|
257
|
+
def self.mark_preset_sub_steps_origin(step)
|
|
258
|
+
return step unless step.is_a?(Hash)
|
|
259
|
+
|
|
260
|
+
sub_steps = step["sub_steps"] || step["sub-steps"]
|
|
261
|
+
return step unless sub_steps.is_a?(Array) && sub_steps.any?
|
|
262
|
+
|
|
263
|
+
step.merge("sub_steps_origin" => "preset")
|
|
264
|
+
end
|
|
265
|
+
private_class_method :mark_preset_sub_steps_origin
|
|
266
|
+
|
|
256
267
|
# Substitute {{placeholder}} tokens in a string.
|
|
257
268
|
#
|
|
258
269
|
# @param text [String, nil] Text with placeholders
|
|
@@ -90,6 +90,7 @@ module Ace
|
|
|
90
90
|
lines = current_for_display.stall_reason.to_s.strip.lines
|
|
91
91
|
puts "Stall Reason: #{lines.first&.chomp}"
|
|
92
92
|
lines[1..].each { |l| puts " #{l.chomp}" } if lines.length > 1
|
|
93
|
+
print_hitl_stall_guidance(lines.first.to_s)
|
|
93
94
|
end
|
|
94
95
|
if current_for_display.workflow
|
|
95
96
|
puts "Workflow: #{current_for_display.workflow}"
|
|
@@ -369,6 +370,29 @@ module Ace
|
|
|
369
370
|
puts
|
|
370
371
|
end
|
|
371
372
|
|
|
373
|
+
def print_hitl_stall_guidance(first_line)
|
|
374
|
+
hitl = parse_hitl_stall_reason(first_line)
|
|
375
|
+
return unless hitl
|
|
376
|
+
|
|
377
|
+
puts "HITL Guidance:"
|
|
378
|
+
puts " Review event: ace-hitl show #{hitl[:id]}"
|
|
379
|
+
puts " Stored path: #{hitl[:path]}" if hitl[:path]
|
|
380
|
+
puts " Requester default: ace-hitl wait #{hitl[:id]}"
|
|
381
|
+
puts " Fallback dispatch: ace-hitl update #{hitl[:id]} --answer \"<decision>\" --resume"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def parse_hitl_stall_reason(line)
|
|
385
|
+
stripped = line.to_s.strip
|
|
386
|
+
return nil unless stripped.start_with?("HITL:")
|
|
387
|
+
|
|
388
|
+
payload = stripped.sub(/^HITL:\s*/, "")
|
|
389
|
+
id, path = payload.split(/\s+/, 2)
|
|
390
|
+
return nil if id.to_s.strip.empty?
|
|
391
|
+
|
|
392
|
+
path = path.to_s.strip
|
|
393
|
+
{id: id, path: path.empty? ? nil : path}
|
|
394
|
+
end
|
|
395
|
+
|
|
372
396
|
# Print other assignments section
|
|
373
397
|
def print_other_assignments(current_assignment_id, include_completed:)
|
|
374
398
|
discoverer = Molecules::AssignmentDiscoverer.new
|
|
@@ -297,13 +297,16 @@ module Ace
|
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
def discover_protocol_source_paths(protocol:, package_glob:)
|
|
300
|
-
registry_paths =
|
|
301
|
-
registry = Ace::Support::Nav::Molecules::SourceRegistry.new
|
|
300
|
+
registry_paths = with_project_root do
|
|
301
|
+
registry = Ace::Support::Nav::Molecules::SourceRegistry.new(start_path: project_root)
|
|
302
302
|
registry.sources_for_protocol(protocol).filter_map do |source|
|
|
303
303
|
next if source.config.is_a?(Hash) && source.config["enabled"] == false
|
|
304
304
|
|
|
305
305
|
candidate = resolve_source_directory(source)
|
|
306
|
-
File.directory?(candidate)
|
|
306
|
+
next unless File.directory?(candidate)
|
|
307
|
+
next if external_implicit_source?(source, candidate)
|
|
308
|
+
|
|
309
|
+
candidate
|
|
307
310
|
rescue
|
|
308
311
|
nil
|
|
309
312
|
end
|
|
@@ -312,6 +315,12 @@ module Ace
|
|
|
312
315
|
(registry_paths + discover_package_default_source_paths(package_glob)).uniq
|
|
313
316
|
end
|
|
314
317
|
|
|
318
|
+
def with_project_root
|
|
319
|
+
Dir.chdir(project_root) { yield }
|
|
320
|
+
rescue Errno::ENOENT
|
|
321
|
+
yield
|
|
322
|
+
end
|
|
323
|
+
|
|
315
324
|
def resolve_source_directory(source)
|
|
316
325
|
candidate = source.full_path
|
|
317
326
|
return candidate if File.directory?(candidate)
|
|
@@ -326,6 +335,23 @@ module Ace
|
|
|
326
335
|
File.directory?(fallback) ? fallback : nil
|
|
327
336
|
end
|
|
328
337
|
|
|
338
|
+
def external_implicit_source?(source, directory)
|
|
339
|
+
return false if path_within_project?(directory)
|
|
340
|
+
return false if explicit_registration?(source)
|
|
341
|
+
|
|
342
|
+
true
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def explicit_registration?(source)
|
|
346
|
+
%w[project user].include?(source.origin.to_s)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def path_within_project?(path)
|
|
350
|
+
candidate = Pathname.new(File.expand_path(path))
|
|
351
|
+
root = Pathname.new(File.expand_path(project_root))
|
|
352
|
+
candidate == root || candidate.to_s.start_with?("#{root}/")
|
|
353
|
+
end
|
|
354
|
+
|
|
329
355
|
def discover_package_default_source_paths(source_glob)
|
|
330
356
|
source_files = Dir.glob(source_glob).sort
|
|
331
357
|
source_files.filter_map do |source_file|
|
|
@@ -479,7 +479,11 @@ module Ace
|
|
|
479
479
|
next step unless step.is_a?(Hash)
|
|
480
480
|
|
|
481
481
|
sub_steps = step["sub_steps"] || step["sub-steps"]
|
|
482
|
-
|
|
482
|
+
if sub_steps.is_a?(Array) && sub_steps.any?
|
|
483
|
+
explicit = step.dup
|
|
484
|
+
explicit["sub_steps_origin"] ||= "explicit"
|
|
485
|
+
next explicit
|
|
486
|
+
end
|
|
483
487
|
|
|
484
488
|
assign_config = resolve_step_assign_config(step)
|
|
485
489
|
next step unless assign_config
|
|
@@ -487,7 +491,10 @@ module Ace
|
|
|
487
491
|
resolved_sub_steps = assign_config[:sub_steps]
|
|
488
492
|
next step unless resolved_sub_steps.is_a?(Array) && resolved_sub_steps.any?
|
|
489
493
|
|
|
490
|
-
enriched = step.merge(
|
|
494
|
+
enriched = step.merge(
|
|
495
|
+
"sub_steps" => resolved_sub_steps,
|
|
496
|
+
"sub_steps_origin" => "inferred"
|
|
497
|
+
)
|
|
491
498
|
enriched["context"] ||= assign_config[:context] if assign_config[:context]
|
|
492
499
|
enriched
|
|
493
500
|
end
|
|
@@ -523,6 +530,7 @@ module Ace
|
|
|
523
530
|
# Create split parent orchestration node
|
|
524
531
|
parent_context = step["context"] || "fork"
|
|
525
532
|
parent_instructions = step["instructions"]
|
|
533
|
+
sub_steps_origin = step["sub_steps_origin"] || "explicit"
|
|
526
534
|
parent_step = build_split_parent_step(
|
|
527
535
|
step: step,
|
|
528
536
|
parent_number: parent_number,
|
|
@@ -540,7 +548,8 @@ module Ace
|
|
|
540
548
|
parent_number: parent_number,
|
|
541
549
|
parent_step: step,
|
|
542
550
|
parent_instructions: parent_instructions,
|
|
543
|
-
parent_context: parent_context
|
|
551
|
+
parent_context: parent_context,
|
|
552
|
+
sub_steps_origin: sub_steps_origin
|
|
544
553
|
)
|
|
545
554
|
end
|
|
546
555
|
else
|
|
@@ -680,8 +689,9 @@ module Ace
|
|
|
680
689
|
# @param parent_step [Hash] Parent step config
|
|
681
690
|
# @param parent_instructions [String, Array<String>, nil] Parent instructions
|
|
682
691
|
# @param parent_context [String, nil] Parent execution context
|
|
692
|
+
# @param sub_steps_origin [String] Whether the subtree was declared explicitly or inferred
|
|
683
693
|
# @return [Hash] Child step config
|
|
684
|
-
def build_child_sub_step(sub_name:, child_number:, parent_number:, parent_step:, parent_instructions:, parent_context:)
|
|
694
|
+
def build_child_sub_step(sub_name:, child_number:, parent_number:, parent_step:, parent_instructions:, parent_context:, sub_steps_origin: "inferred")
|
|
685
695
|
step_def = find_step_definition(sub_name)
|
|
686
696
|
parent_task_ref = extract_parent_taskref(parent_step, parent_instructions)
|
|
687
697
|
instructions = if step_def&.dig("skill")
|
|
@@ -693,16 +703,18 @@ module Ace
|
|
|
693
703
|
"number" => child_number,
|
|
694
704
|
"name" => sub_name,
|
|
695
705
|
"instructions" => instructions,
|
|
696
|
-
"parent" => parent_number
|
|
706
|
+
"parent" => parent_number,
|
|
707
|
+
"sub_steps_origin" => sub_steps_origin
|
|
697
708
|
}
|
|
698
709
|
child["taskref"] = parent_task_ref if parent_task_ref
|
|
699
710
|
|
|
700
711
|
if step_def
|
|
701
712
|
child["workflow"] = step_def["workflow"] if step_def["workflow"]
|
|
702
|
-
|
|
713
|
+
preserve_explicit_skill = (sub_steps_origin == "explicit")
|
|
714
|
+
child["skill"] = step_def["skill"] if step_def["skill"] && (preserve_explicit_skill || !step_def["workflow"])
|
|
703
715
|
|
|
704
716
|
context_default = step_def.dig("context", "default")
|
|
705
|
-
child["context"] = context_default if context_default && parent_context
|
|
717
|
+
child["context"] = context_default if context_default && !fork_context_value?(parent_context)
|
|
706
718
|
fork_context = step_def.dig("context", "fork")
|
|
707
719
|
if child["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
|
|
708
720
|
# Generated child sub-steps have no explicit frontmatter overrides.
|
|
@@ -849,18 +861,20 @@ module Ace
|
|
|
849
861
|
"instructions" => rendered_instructions,
|
|
850
862
|
"workflow" => rendering["workflow"]
|
|
851
863
|
)
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
864
|
+
unless split_child_without_explicit_fork?(step)
|
|
865
|
+
context_default = rendering.dig("context", "default")
|
|
866
|
+
materialized["context"] ||= context_default if context_default
|
|
867
|
+
fork_context = rendering.dig("context", "fork")
|
|
868
|
+
if materialized["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
|
|
869
|
+
# For materialized explicit steps, preserve frontmatter-provided fork config
|
|
870
|
+
# (`||=` semantics). Rendering contributes defaults only when the step
|
|
871
|
+
# itself did not declare fork options.
|
|
872
|
+
materialized["fork"] ||= fork_context
|
|
873
|
+
end
|
|
860
874
|
end
|
|
861
875
|
materialized["source_skill"] = rendering["source_skill"] || rendering["skill"] if rendering["source_skill"] || rendering["skill"]
|
|
862
876
|
materialized["source_workflow"] = rendering["workflow"] if rendering["workflow"] && !rendering["workflow"].empty?
|
|
863
|
-
materialized.delete("skill")
|
|
877
|
+
materialized.delete("skill") unless preserve_explicit_child_skill?(step)
|
|
864
878
|
materialized
|
|
865
879
|
end
|
|
866
880
|
|
|
@@ -868,6 +882,11 @@ module Ace
|
|
|
868
882
|
explicit_workflow = step["workflow"]&.to_s&.strip
|
|
869
883
|
if explicit_workflow && !explicit_workflow.empty?
|
|
870
884
|
canonical_step = find_step_definition(step["name"]&.to_s)
|
|
885
|
+
if canonical_step && split_child_without_explicit_fork?(step)
|
|
886
|
+
canonical_step = canonical_step.dup
|
|
887
|
+
canonical_step.delete("context")
|
|
888
|
+
canonical_step.delete("fork")
|
|
889
|
+
end
|
|
871
890
|
source_skill = step["source_skill"]&.to_s&.strip
|
|
872
891
|
source_skill = canonical_step&.dig("source_skill") if source_skill.nil? || source_skill.empty?
|
|
873
892
|
rendering = skill_source_resolver.resolve_workflow_rendering(
|
|
@@ -893,6 +912,20 @@ module Ace
|
|
|
893
912
|
skill_source_resolver.resolve_step_rendering(step["name"]&.to_s)
|
|
894
913
|
end
|
|
895
914
|
|
|
915
|
+
def split_child_without_explicit_fork?(step)
|
|
916
|
+
step["parent"] && !step.key?("context") && !step.key?("fork")
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def preserve_explicit_child_skill?(step)
|
|
920
|
+
step["parent"] && step["sub_steps_origin"] == "explicit"
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def fork_context_value?(value)
|
|
924
|
+
normalized = value.to_s.strip.downcase
|
|
925
|
+
normalized = normalized.delete_prefix(":")
|
|
926
|
+
normalized == "fork"
|
|
927
|
+
end
|
|
928
|
+
|
|
896
929
|
def render_skill_backed_step_instructions(step:, rendering:)
|
|
897
930
|
if step_render_mode(rendering) == "step_template"
|
|
898
931
|
return render_step_template_instructions(step: step, rendering: rendering)
|
|
@@ -1201,6 +1234,7 @@ module Ace
|
|
|
1201
1234
|
|
|
1202
1235
|
def insert_batch_step_tree(step_config, after:, as_child:, added_by:, location:)
|
|
1203
1236
|
normalized = normalize_batch_step_hash(step_config, location: location)
|
|
1237
|
+
normalized = apply_inferred_parent_for_sibling_insert(normalized, after: after, as_child: as_child)
|
|
1204
1238
|
|
|
1205
1239
|
if canonical_batch_insert_requested?(normalized)
|
|
1206
1240
|
canonical_inserted = insert_canonical_batch_step_tree(
|
|
@@ -1456,6 +1490,27 @@ module Ace
|
|
|
1456
1490
|
end
|
|
1457
1491
|
end
|
|
1458
1492
|
|
|
1493
|
+
def apply_inferred_parent_for_sibling_insert(step_config, after:, as_child:)
|
|
1494
|
+
return step_config unless step_config.is_a?(Hash)
|
|
1495
|
+
return step_config if as_child
|
|
1496
|
+
return step_config if after.nil? || after.to_s.strip.empty?
|
|
1497
|
+
return step_config if step_config.key?("parent")
|
|
1498
|
+
|
|
1499
|
+
inferred_parent = infer_parent_from_anchor(after)
|
|
1500
|
+
return step_config if inferred_parent.nil? || inferred_parent.to_s.strip.empty?
|
|
1501
|
+
|
|
1502
|
+
step_config.merge("parent" => inferred_parent)
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
def infer_parent_from_anchor(anchor_number)
|
|
1506
|
+
assignment = assignment_manager.find_active
|
|
1507
|
+
return nil unless assignment
|
|
1508
|
+
|
|
1509
|
+
state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
|
|
1510
|
+
anchor = state.find_by_number(anchor_number.to_s.strip)
|
|
1511
|
+
anchor&.parent
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1459
1514
|
def default_dynamic_step_instructions
|
|
1460
1515
|
DEFAULT_DYNAMIC_STEP_INSTRUCTIONS
|
|
1461
1516
|
end
|
data/lib/ace/assign/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-assign
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.42.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|