ace-assign 0.41.5 → 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/mark-task-done.step.yml +5 -5
- 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/.ace-defaults/assign/config.yml +0 -11
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -6
- data/CHANGELOG.md +88 -0
- data/README.md +2 -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/models/queue_state.rb +1 -0
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +31 -12
- data/lib/ace/assign/organisms/assignment_executor.rb +72 -20
- 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
|
|
@@ -20,7 +20,7 @@ instructions: |
|
|
|
20
20
|
|
|
21
21
|
1. Run the status update command:
|
|
22
22
|
```bash
|
|
23
|
-
ace-task update {{taskref}} --set status=done --move-to archive
|
|
23
|
+
ace-task update {{taskref}} --set status=done --move-to archive --git-commit
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
2. **VERIFY the status change** (do not skip):
|
|
@@ -38,16 +38,16 @@ instructions: |
|
|
|
38
38
|
```
|
|
39
39
|
If all subtasks show `✓` (done), mark the parent done too:
|
|
40
40
|
```bash
|
|
41
|
-
ace-task update {{parent_taskref}} --set status=done --move-to archive
|
|
41
|
+
ace-task update {{parent_taskref}} --set status=done --move-to archive --git-commit
|
|
42
42
|
ace-task show {{parent_taskref}}
|
|
43
43
|
```
|
|
44
44
|
Repeat upward if the parent itself has a parent.
|
|
45
45
|
|
|
46
|
-
4. **
|
|
46
|
+
4. **Optional validation**: verify the moved task is now archived and complete:
|
|
47
47
|
```bash
|
|
48
|
-
ace-
|
|
48
|
+
ace-task show {{taskref}}
|
|
49
|
+
ace-task show {{parent_taskref}}
|
|
49
50
|
```
|
|
50
|
-
The `--move-to archive` flag relocates task files. This commit captures those moves.
|
|
51
51
|
|
|
52
52
|
when_to_skip:
|
|
53
53
|
- "Task was already marked done (verified via ace-task show)"
|
|
@@ -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"
|
|
@@ -162,12 +162,7 @@ steps:
|
|
|
162
162
|
- `TASKREFS="{{taskrefs}}"`
|
|
163
163
|
- `IFS=',' read -ra refs <<< "${TASKREFS// /}"`
|
|
164
164
|
- For each `ref`:
|
|
165
|
-
- `ace-task update "$ref" --set status=done --move-to archive`
|
|
166
|
-
- |
|
|
167
|
-
After all tasks are archived, commit the file moves:
|
|
168
|
-
```bash
|
|
169
|
-
ace-git-commit .ace-tasks/ -i "archive completed tasks"
|
|
170
|
-
```
|
|
165
|
+
- `ace-task update "$ref" --set status=done --move-to archive --gc`
|
|
171
166
|
|
|
172
167
|
- name: create-retro
|
|
173
168
|
number: "160"
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,94 @@ 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
|
+
|
|
69
|
+
## [0.41.10] - 2026-03-31
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
- Role-based assignment execution defaults.
|
|
73
|
+
|
|
74
|
+
## [0.41.9] - 2026-03-31
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
- Updated task archival commands to persist task-state transitions during workflow execution using `--gc`/`--git-commit` modes in terminal task completion steps.
|
|
78
|
+
|
|
79
|
+
## [0.41.8] - 2026-03-30
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- Clarified the shipped `wfi://release/publish` workflow release contract so root changelog updates consistently include package versions and RubyGems propagation proof guidance.
|
|
83
|
+
|
|
84
|
+
## [0.41.7] - 2026-03-30
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- Made the shipped `wfi://release/publish` workflow verify modified packages with package-scoped `ace-test` execution derived from the resolved release set.
|
|
88
|
+
|
|
89
|
+
## [0.41.6] - 2026-03-29
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
- Added a shipped generic `wfi://release/publish` workflow under `ace-assign` so `work-on-task` release steps have a default resolvable path in plain projects.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
- Aligned `assign.source` `wfi://` resolution with registered/default nav workflow sources by removing implicit workspace workflow-directory fallback behavior.
|
|
96
|
+
- Added resolver coverage for project-level `wfi://` workflow overrides and for unregistered-workflow failure behavior.
|
|
97
|
+
|
|
10
98
|
## [0.41.5] - 2026-03-29
|
|
11
99
|
|
|
12
100
|
### Technical
|
data/README.md
CHANGED
|
@@ -78,6 +78,8 @@ 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
|
+
`work-on-task` release steps resolve `wfi://release/publish` from shipped workflow sources by default, and project-level `wfi://` source overrides registered under `.ace/nav/protocols/wfi-sources/` are honored by both `ace-bundle` and `ace-assign`.
|
|
82
|
+
|
|
81
83
|
**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
84
|
|
|
83
85
|
**Recover from failure without losing history** - keep failed-step lineage intact, inject targeted retries or fix steps, and continue execution with auditable failure evidence.
|
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
|
|
@@ -25,12 +25,9 @@ module Ace
|
|
|
25
25
|
canonical_paths = discover_canonical_skill_source_paths
|
|
26
26
|
canonical_workflow_paths = discover_canonical_workflow_source_paths
|
|
27
27
|
override_paths = normalize_paths(configured_skill_paths || [])
|
|
28
|
-
|
|
29
|
-
configured_workflow_paths = discover_workspace_workflow_paths
|
|
30
|
-
end
|
|
31
|
-
configured_workflow_paths = canonical_workflow_paths if configured_workflow_paths.nil? || configured_workflow_paths.empty?
|
|
28
|
+
configured_workflow_paths = normalize_paths(configured_workflow_paths || [])
|
|
32
29
|
@skill_paths = (canonical_paths + override_paths).uniq
|
|
33
|
-
@workflow_paths = (canonical_workflow_paths +
|
|
30
|
+
@workflow_paths = (canonical_workflow_paths + configured_workflow_paths).uniq
|
|
34
31
|
@skill_index = nil
|
|
35
32
|
end
|
|
36
33
|
|
|
@@ -300,13 +297,16 @@ module Ace
|
|
|
300
297
|
end
|
|
301
298
|
|
|
302
299
|
def discover_protocol_source_paths(protocol:, package_glob:)
|
|
303
|
-
registry_paths =
|
|
304
|
-
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)
|
|
305
302
|
registry.sources_for_protocol(protocol).filter_map do |source|
|
|
306
303
|
next if source.config.is_a?(Hash) && source.config["enabled"] == false
|
|
307
304
|
|
|
308
305
|
candidate = resolve_source_directory(source)
|
|
309
|
-
File.directory?(candidate)
|
|
306
|
+
next unless File.directory?(candidate)
|
|
307
|
+
next if external_implicit_source?(source, candidate)
|
|
308
|
+
|
|
309
|
+
candidate
|
|
310
310
|
rescue
|
|
311
311
|
nil
|
|
312
312
|
end
|
|
@@ -315,6 +315,12 @@ module Ace
|
|
|
315
315
|
(registry_paths + discover_package_default_source_paths(package_glob)).uniq
|
|
316
316
|
end
|
|
317
317
|
|
|
318
|
+
def with_project_root
|
|
319
|
+
Dir.chdir(project_root) { yield }
|
|
320
|
+
rescue Errno::ENOENT
|
|
321
|
+
yield
|
|
322
|
+
end
|
|
323
|
+
|
|
318
324
|
def resolve_source_directory(source)
|
|
319
325
|
candidate = source.full_path
|
|
320
326
|
return candidate if File.directory?(candidate)
|
|
@@ -329,6 +335,23 @@ module Ace
|
|
|
329
335
|
File.directory?(fallback) ? fallback : nil
|
|
330
336
|
end
|
|
331
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
|
+
|
|
332
355
|
def discover_package_default_source_paths(source_glob)
|
|
333
356
|
source_files = Dir.glob(source_glob).sort
|
|
334
357
|
source_files.filter_map do |source_file|
|
|
@@ -344,10 +367,6 @@ module Ace
|
|
|
344
367
|
end
|
|
345
368
|
end
|
|
346
369
|
|
|
347
|
-
def discover_workspace_workflow_paths
|
|
348
|
-
Dir.glob(File.join(project_root, "*", "handbook", "workflow-instructions")).sort
|
|
349
|
-
end
|
|
350
|
-
|
|
351
370
|
def resolve_source_uri(uri, skill_name)
|
|
352
371
|
if uri.start_with?("wfi://")
|
|
353
372
|
resolve_wfi_uri(uri, skill_name)
|
|
@@ -381,9 +381,6 @@ module Ace
|
|
|
381
381
|
raise Error, "Child insertion requires an after step reference."
|
|
382
382
|
end
|
|
383
383
|
|
|
384
|
-
source_label = source_file.to_s.strip.empty? ? "batch" : File.basename(source_file.to_s)
|
|
385
|
-
batch_added_by = "batch_from:#{source_label}"
|
|
386
|
-
|
|
387
384
|
prevalidate_batch_trees!(steps)
|
|
388
385
|
|
|
389
386
|
added_steps = []
|
|
@@ -395,7 +392,7 @@ module Ace
|
|
|
395
392
|
step_config,
|
|
396
393
|
after: as_child ? after : sibling_cursor,
|
|
397
394
|
as_child: as_child,
|
|
398
|
-
added_by:
|
|
395
|
+
added_by: nil,
|
|
399
396
|
location: "steps[#{index}]"
|
|
400
397
|
)
|
|
401
398
|
added_steps.concat(inserted[:added])
|
|
@@ -482,7 +479,11 @@ module Ace
|
|
|
482
479
|
next step unless step.is_a?(Hash)
|
|
483
480
|
|
|
484
481
|
sub_steps = step["sub_steps"] || step["sub-steps"]
|
|
485
|
-
|
|
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
|
|
486
487
|
|
|
487
488
|
assign_config = resolve_step_assign_config(step)
|
|
488
489
|
next step unless assign_config
|
|
@@ -490,7 +491,10 @@ module Ace
|
|
|
490
491
|
resolved_sub_steps = assign_config[:sub_steps]
|
|
491
492
|
next step unless resolved_sub_steps.is_a?(Array) && resolved_sub_steps.any?
|
|
492
493
|
|
|
493
|
-
enriched = step.merge(
|
|
494
|
+
enriched = step.merge(
|
|
495
|
+
"sub_steps" => resolved_sub_steps,
|
|
496
|
+
"sub_steps_origin" => "inferred"
|
|
497
|
+
)
|
|
494
498
|
enriched["context"] ||= assign_config[:context] if assign_config[:context]
|
|
495
499
|
enriched
|
|
496
500
|
end
|
|
@@ -526,6 +530,7 @@ module Ace
|
|
|
526
530
|
# Create split parent orchestration node
|
|
527
531
|
parent_context = step["context"] || "fork"
|
|
528
532
|
parent_instructions = step["instructions"]
|
|
533
|
+
sub_steps_origin = step["sub_steps_origin"] || "explicit"
|
|
529
534
|
parent_step = build_split_parent_step(
|
|
530
535
|
step: step,
|
|
531
536
|
parent_number: parent_number,
|
|
@@ -543,7 +548,8 @@ module Ace
|
|
|
543
548
|
parent_number: parent_number,
|
|
544
549
|
parent_step: step,
|
|
545
550
|
parent_instructions: parent_instructions,
|
|
546
|
-
parent_context: parent_context
|
|
551
|
+
parent_context: parent_context,
|
|
552
|
+
sub_steps_origin: sub_steps_origin
|
|
547
553
|
)
|
|
548
554
|
end
|
|
549
555
|
else
|
|
@@ -683,8 +689,9 @@ module Ace
|
|
|
683
689
|
# @param parent_step [Hash] Parent step config
|
|
684
690
|
# @param parent_instructions [String, Array<String>, nil] Parent instructions
|
|
685
691
|
# @param parent_context [String, nil] Parent execution context
|
|
692
|
+
# @param sub_steps_origin [String] Whether the subtree was declared explicitly or inferred
|
|
686
693
|
# @return [Hash] Child step config
|
|
687
|
-
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")
|
|
688
695
|
step_def = find_step_definition(sub_name)
|
|
689
696
|
parent_task_ref = extract_parent_taskref(parent_step, parent_instructions)
|
|
690
697
|
instructions = if step_def&.dig("skill")
|
|
@@ -696,16 +703,18 @@ module Ace
|
|
|
696
703
|
"number" => child_number,
|
|
697
704
|
"name" => sub_name,
|
|
698
705
|
"instructions" => instructions,
|
|
699
|
-
"parent" => parent_number
|
|
706
|
+
"parent" => parent_number,
|
|
707
|
+
"sub_steps_origin" => sub_steps_origin
|
|
700
708
|
}
|
|
701
709
|
child["taskref"] = parent_task_ref if parent_task_ref
|
|
702
710
|
|
|
703
711
|
if step_def
|
|
704
712
|
child["workflow"] = step_def["workflow"] if step_def["workflow"]
|
|
705
|
-
|
|
713
|
+
preserve_explicit_skill = (sub_steps_origin == "explicit")
|
|
714
|
+
child["skill"] = step_def["skill"] if step_def["skill"] && (preserve_explicit_skill || !step_def["workflow"])
|
|
706
715
|
|
|
707
716
|
context_default = step_def.dig("context", "default")
|
|
708
|
-
child["context"] = context_default if context_default && parent_context
|
|
717
|
+
child["context"] = context_default if context_default && !fork_context_value?(parent_context)
|
|
709
718
|
fork_context = step_def.dig("context", "fork")
|
|
710
719
|
if child["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
|
|
711
720
|
# Generated child sub-steps have no explicit frontmatter overrides.
|
|
@@ -852,18 +861,20 @@ module Ace
|
|
|
852
861
|
"instructions" => rendered_instructions,
|
|
853
862
|
"workflow" => rendering["workflow"]
|
|
854
863
|
)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
|
863
874
|
end
|
|
864
875
|
materialized["source_skill"] = rendering["source_skill"] || rendering["skill"] if rendering["source_skill"] || rendering["skill"]
|
|
865
876
|
materialized["source_workflow"] = rendering["workflow"] if rendering["workflow"] && !rendering["workflow"].empty?
|
|
866
|
-
materialized.delete("skill")
|
|
877
|
+
materialized.delete("skill") unless preserve_explicit_child_skill?(step)
|
|
867
878
|
materialized
|
|
868
879
|
end
|
|
869
880
|
|
|
@@ -871,6 +882,11 @@ module Ace
|
|
|
871
882
|
explicit_workflow = step["workflow"]&.to_s&.strip
|
|
872
883
|
if explicit_workflow && !explicit_workflow.empty?
|
|
873
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
|
|
874
890
|
source_skill = step["source_skill"]&.to_s&.strip
|
|
875
891
|
source_skill = canonical_step&.dig("source_skill") if source_skill.nil? || source_skill.empty?
|
|
876
892
|
rendering = skill_source_resolver.resolve_workflow_rendering(
|
|
@@ -896,6 +912,20 @@ module Ace
|
|
|
896
912
|
skill_source_resolver.resolve_step_rendering(step["name"]&.to_s)
|
|
897
913
|
end
|
|
898
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
|
+
|
|
899
929
|
def render_skill_backed_step_instructions(step:, rendering:)
|
|
900
930
|
if step_render_mode(rendering) == "step_template"
|
|
901
931
|
return render_step_template_instructions(step: step, rendering: rendering)
|
|
@@ -1204,6 +1234,7 @@ module Ace
|
|
|
1204
1234
|
|
|
1205
1235
|
def insert_batch_step_tree(step_config, after:, as_child:, added_by:, location:)
|
|
1206
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)
|
|
1207
1238
|
|
|
1208
1239
|
if canonical_batch_insert_requested?(normalized)
|
|
1209
1240
|
canonical_inserted = insert_canonical_batch_step_tree(
|
|
@@ -1459,6 +1490,27 @@ module Ace
|
|
|
1459
1490
|
end
|
|
1460
1491
|
end
|
|
1461
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
|
+
|
|
1462
1514
|
def default_dynamic_step_instructions
|
|
1463
1515
|
DEFAULT_DYNAMIC_STEP_INSTRUCTIONS
|
|
1464
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-
|
|
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
|