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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '06674279e2b0b541d4da053a4bb8e385ea6e76b8d8cb58e6fc4b5032dac2e15e'
4
- data.tar.gz: aa8e3423d01ebe3b86e355474b1a77e506cc1efcdf68f953e449f9bc671fbd72
3
+ metadata.gz: 214f383018bb2b9861e03cedf52c8eca4df627d9787357d00998725be36cbe36
4
+ data.tar.gz: 5f8d577f6f0eacd399c2a317d8dee720f237194af70968b65fcca8a1de676dea
5
5
  SHA512:
6
- metadata.gz: 51a4be473d02363c49c5439b4f4814c4c22dddfa30d2c7674aeae68f2c29a98f9dfe44440cd26aa4d5fc1633f5918d045cc24a190ab68a38e41d797a95e3e00f
7
- data.tar.gz: f97ce64ca4fecb86f9bdad9312f6864e70f5cbe098c8f00a4db408d77e91b4d0691f36868b6728238231e2ce923effd3e73c1f242d2c94cdb16e18f3a89526d9
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, and fork execution.
4
+ purpose: Complete command reference for ace-assign queue orchestration, hierarchy,
5
+ and fork execution.
5
6
  ace-docs:
6
- last-updated: 2026-03-26
7
- last-checked: 2026-03-26
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 = Dir.chdir(project_root) do
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) ? candidate : nil
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
- next step if sub_steps.is_a?(Array) && sub_steps.any?
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("sub_steps" => resolved_sub_steps)
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
- child["skill"] = step_def["skill"] if step_def["skill"] && !step_def["workflow"]
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 != "fork"
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
- context_default = rendering.dig("context", "default")
853
- materialized["context"] ||= context_default if context_default
854
- fork_context = rendering.dig("context", "fork")
855
- if materialized["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
856
- # For materialized explicit steps, preserve frontmatter-provided fork config
857
- # (`||=` semantics). Rendering contributes defaults only when the step
858
- # itself did not declare fork options.
859
- materialized["fork"] ||= fork_context
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Assign
5
- VERSION = "0.41.10"
5
+ VERSION = "0.42.4"
6
6
  end
7
7
  end
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.41.10
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-01 00:00:00.000000000 Z
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