ace-assign 0.41.10 → 0.53.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/composition-rules.yml +2 -17
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +0 -26
- data/.ace-defaults/assign/catalog/steps/create-retro.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-task-done.step.yml +1 -2
- data/.ace-defaults/assign/catalog/steps/onboard.step.yml +0 -17
- data/.ace-defaults/assign/catalog/steps/plan-task.step.yml +0 -15
- data/.ace-defaults/assign/catalog/steps/pre-commit-review.step.yml +3 -0
- data/.ace-defaults/assign/catalog/steps/reflect-and-refactor.step.yml +3 -2
- data/.ace-defaults/assign/catalog/steps/review-pr.step.yml +0 -20
- data/.ace-defaults/assign/catalog/steps/task-load.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/verify-test-suite.step.yml +7 -34
- data/.ace-defaults/assign/catalog/steps/verify-test.step.yml +7 -4
- data/.ace-defaults/assign/catalog/steps/work-on-task.step.yml +0 -21
- data/.ace-defaults/assign/presets/fix-bug.yml +4 -3
- data/.ace-defaults/assign/presets/quick-implement.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +3 -16
- data/CHANGELOG.md +260 -0
- data/README.md +20 -43
- data/docs/demo/canonical-skill-source.gif +0 -0
- data/docs/demo/canonical-skill-source.tape.yml +51 -0
- data/docs/demo/fork-provider.cast +957 -0
- data/docs/demo/fork-provider.gif +0 -0
- data/docs/demo/fork-provider.recording.json +32 -0
- data/docs/demo/fork-provider.tape.yml +65 -20
- data/docs/getting-started.md +5 -2
- data/docs/usage.md +63 -3
- data/handbook/guides/fork-context.g.md +2 -2
- data/handbook/skills/as-assign-drive/SKILL.md +13 -1
- data/handbook/skills/as-create-retro-internal/SKILL.md +29 -0
- data/handbook/skills/as-mark-task-done-internal/SKILL.md +29 -0
- data/handbook/skills/as-reflect-and-refactor-internal/SKILL.md +30 -0
- data/handbook/skills/as-task-load-internal/SKILL.md +28 -0
- data/handbook/workflow-instructions/assign/compose.wf.md +3 -3
- data/handbook/workflow-instructions/assign/create-retro-internal.wf.md +11 -0
- data/handbook/workflow-instructions/assign/create.wf.md +6 -3
- data/handbook/workflow-instructions/assign/drive.wf.md +273 -15
- data/handbook/workflow-instructions/assign/mark-task-done-internal.wf.md +12 -0
- data/handbook/workflow-instructions/assign/prepare.wf.md +5 -5
- data/handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md +14 -0
- data/handbook/workflow-instructions/assign/run-in-batches.wf.md +4 -1
- data/handbook/workflow-instructions/assign/start.wf.md +5 -2
- data/handbook/workflow-instructions/assign/task-load-internal.wf.md +12 -0
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +36 -0
- data/lib/ace/assign/atoms/catalog_loader.rb +105 -2
- data/lib/ace/assign/atoms/preset_expander.rb +12 -1
- data/lib/ace/assign/atoms/step_file_parser.rb +15 -0
- data/lib/ace/assign/cli/commands/assignment_target.rb +53 -0
- data/lib/ace/assign/cli/commands/finish.rb +7 -4
- data/lib/ace/assign/cli/commands/fork_run.rb +4 -1
- data/lib/ace/assign/cli/commands/fork_session.rb +52 -0
- data/lib/ace/assign/cli/commands/start.rb +9 -3
- data/lib/ace/assign/cli/commands/status.rb +231 -226
- data/lib/ace/assign/cli/commands/step.rb +62 -0
- data/lib/ace/assign/cli.rb +8 -1
- data/lib/ace/assign/models/step.rb +4 -2
- data/lib/ace/assign/molecules/fork_session_launcher.rb +189 -8
- data/lib/ace/assign/molecules/queue_scanner.rb +1 -0
- data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +252 -50
- data/lib/ace/assign/molecules/tmux_fork_runner.rb +191 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +294 -40
- data/lib/ace/assign/version.rb +1 -1
- metadata +21 -5
- data/.ace-defaults/assign/catalog/steps/verify-e2e.step.yml +0 -42
|
@@ -12,15 +12,40 @@ module Ace
|
|
|
12
12
|
# start → advance → complete (with fail/add/retry branches)
|
|
13
13
|
class AssignmentExecutor
|
|
14
14
|
DEFAULT_DYNAMIC_STEP_INSTRUCTIONS = "Complete this step and finish with: ace-assign finish --message report.md".freeze
|
|
15
|
+
PROJECT_ROOT_SIGNAL = "project_root".freeze
|
|
16
|
+
CATALOG_SIGNAL = "catalog".freeze
|
|
15
17
|
|
|
16
18
|
attr_reader :assignment_manager, :queue_scanner, :step_writer, :step_renumberer, :skill_source_resolver
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
class << self
|
|
21
|
+
def clear_caches!
|
|
22
|
+
@cache_store = { step_catalog_cache: {} }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def cache_store
|
|
26
|
+
@cache_store ||= { step_catalog_cache: {} }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def cached_value(store_key, key)
|
|
32
|
+
cache_store[store_key][key]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def store_cached_value(store_key, key, value)
|
|
36
|
+
cache_store[store_key][key] = value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(cache_base: nil, skill_source_resolver: nil, step_catalog: nil)
|
|
19
41
|
@assignment_manager = Molecules::AssignmentManager.new(cache_base: cache_base)
|
|
20
42
|
@queue_scanner = Molecules::QueueScanner.new
|
|
21
43
|
@step_writer = Molecules::StepWriter.new
|
|
22
|
-
@skill_source_resolver = Molecules::SkillAssignSourceResolver.new
|
|
44
|
+
@skill_source_resolver = skill_source_resolver || Molecules::SkillAssignSourceResolver.new
|
|
23
45
|
@step_catalog = nil
|
|
46
|
+
@step_catalog_from_fixture = step_catalog
|
|
47
|
+
@step_catalog_from_fixture_set = !step_catalog.nil?
|
|
48
|
+
@step_catalog_loaded = false
|
|
24
49
|
@step_renumberer = Molecules::StepRenumberer.new(
|
|
25
50
|
step_writer: @step_writer,
|
|
26
51
|
queue_scanner: @queue_scanner
|
|
@@ -479,7 +504,11 @@ module Ace
|
|
|
479
504
|
next step unless step.is_a?(Hash)
|
|
480
505
|
|
|
481
506
|
sub_steps = step["sub_steps"] || step["sub-steps"]
|
|
482
|
-
|
|
507
|
+
if sub_steps.is_a?(Array) && sub_steps.any?
|
|
508
|
+
explicit = step.dup
|
|
509
|
+
explicit["sub_steps_origin"] ||= "explicit"
|
|
510
|
+
next explicit
|
|
511
|
+
end
|
|
483
512
|
|
|
484
513
|
assign_config = resolve_step_assign_config(step)
|
|
485
514
|
next step unless assign_config
|
|
@@ -487,7 +516,10 @@ module Ace
|
|
|
487
516
|
resolved_sub_steps = assign_config[:sub_steps]
|
|
488
517
|
next step unless resolved_sub_steps.is_a?(Array) && resolved_sub_steps.any?
|
|
489
518
|
|
|
490
|
-
enriched = step.merge(
|
|
519
|
+
enriched = step.merge(
|
|
520
|
+
"sub_steps" => resolved_sub_steps,
|
|
521
|
+
"sub_steps_origin" => "inferred"
|
|
522
|
+
)
|
|
491
523
|
enriched["context"] ||= assign_config[:context] if assign_config[:context]
|
|
492
524
|
enriched
|
|
493
525
|
end
|
|
@@ -523,6 +555,7 @@ module Ace
|
|
|
523
555
|
# Create split parent orchestration node
|
|
524
556
|
parent_context = step["context"] || "fork"
|
|
525
557
|
parent_instructions = step["instructions"]
|
|
558
|
+
sub_steps_origin = step["sub_steps_origin"] || "explicit"
|
|
526
559
|
parent_step = build_split_parent_step(
|
|
527
560
|
step: step,
|
|
528
561
|
parent_number: parent_number,
|
|
@@ -540,7 +573,8 @@ module Ace
|
|
|
540
573
|
parent_number: parent_number,
|
|
541
574
|
parent_step: step,
|
|
542
575
|
parent_instructions: parent_instructions,
|
|
543
|
-
parent_context: parent_context
|
|
576
|
+
parent_context: parent_context,
|
|
577
|
+
sub_steps_origin: sub_steps_origin
|
|
544
578
|
)
|
|
545
579
|
end
|
|
546
580
|
else
|
|
@@ -564,7 +598,10 @@ module Ace
|
|
|
564
598
|
# @param sub_steps [Array<String>] Declared sub-step names
|
|
565
599
|
# @return [Hash] Parent step config for runtime queue
|
|
566
600
|
def build_split_parent_step(step:, parent_number:, parent_context:, sub_steps:)
|
|
567
|
-
source_skill = step["skill"]
|
|
601
|
+
source_skill = step["source_skill"] || step["skill"]
|
|
602
|
+
if (source_skill.nil? || source_skill.to_s.strip.empty?) && step["source"].to_s.start_with?("skill://")
|
|
603
|
+
source_skill = step["source"].to_s.delete_prefix("skill://").strip
|
|
604
|
+
end
|
|
568
605
|
original_text = normalize_instructions(step["instructions"]).strip
|
|
569
606
|
definition = find_step_definition("split-subtree-root") || {}
|
|
570
607
|
|
|
@@ -591,6 +628,7 @@ module Ace
|
|
|
591
628
|
)
|
|
592
629
|
parent_step.delete("sub_steps")
|
|
593
630
|
parent_step.delete("sub-steps")
|
|
631
|
+
parent_step.delete("source")
|
|
594
632
|
parent_step.delete("skill")
|
|
595
633
|
parent_step.delete("workflow")
|
|
596
634
|
parent_step["source_skill"] = source_skill if source_skill
|
|
@@ -680,8 +718,9 @@ module Ace
|
|
|
680
718
|
# @param parent_step [Hash] Parent step config
|
|
681
719
|
# @param parent_instructions [String, Array<String>, nil] Parent instructions
|
|
682
720
|
# @param parent_context [String, nil] Parent execution context
|
|
721
|
+
# @param sub_steps_origin [String] Whether the subtree was declared explicitly or inferred
|
|
683
722
|
# @return [Hash] Child step config
|
|
684
|
-
def build_child_sub_step(sub_name:, child_number:, parent_number:, parent_step:, parent_instructions:, parent_context:)
|
|
723
|
+
def build_child_sub_step(sub_name:, child_number:, parent_number:, parent_step:, parent_instructions:, parent_context:, sub_steps_origin: "inferred")
|
|
685
724
|
step_def = find_step_definition(sub_name)
|
|
686
725
|
parent_task_ref = extract_parent_taskref(parent_step, parent_instructions)
|
|
687
726
|
instructions = if step_def&.dig("skill")
|
|
@@ -693,16 +732,25 @@ module Ace
|
|
|
693
732
|
"number" => child_number,
|
|
694
733
|
"name" => sub_name,
|
|
695
734
|
"instructions" => instructions,
|
|
696
|
-
"parent" => parent_number
|
|
735
|
+
"parent" => parent_number,
|
|
736
|
+
"sub_steps_origin" => sub_steps_origin
|
|
697
737
|
}
|
|
698
738
|
child["taskref"] = parent_task_ref if parent_task_ref
|
|
699
739
|
|
|
700
740
|
if step_def
|
|
701
741
|
child["workflow"] = step_def["workflow"] if step_def["workflow"]
|
|
702
|
-
|
|
742
|
+
preserve_explicit_skill = (sub_steps_origin == "explicit")
|
|
743
|
+
child["skill"] = step_def["skill"] if step_def["skill"] && (preserve_explicit_skill || !step_def["workflow"])
|
|
744
|
+
child["source"] = if step_def["source"]
|
|
745
|
+
step_def["source"]
|
|
746
|
+
elsif step_def["workflow"]
|
|
747
|
+
step_def["workflow"]
|
|
748
|
+
elsif step_def["skill"]
|
|
749
|
+
"skill://#{step_def["skill"]}"
|
|
750
|
+
end
|
|
703
751
|
|
|
704
752
|
context_default = step_def.dig("context", "default")
|
|
705
|
-
child["context"] = context_default if context_default && parent_context
|
|
753
|
+
child["context"] = context_default if context_default && !fork_context_value?(parent_context)
|
|
706
754
|
fork_context = step_def.dig("context", "fork")
|
|
707
755
|
if child["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
|
|
708
756
|
# Generated child sub-steps have no explicit frontmatter overrides.
|
|
@@ -799,14 +847,9 @@ module Ace
|
|
|
799
847
|
when "pre-commit-review"
|
|
800
848
|
pre_commit_review_action_instructions(task_hint: task_hint)
|
|
801
849
|
when "verify-test"
|
|
802
|
-
"- Identify modified packages#{task_hint}.\n- For each modified package, run: cd <package> && ace-test --profile 6\n- If no package-level code changes are present, mark this step skipped with a clear reason."
|
|
850
|
+
"- Identify modified packages#{task_hint}.\n- For each modified package, run: cd <package> && ace-test all --profile 6\n- This subtree step verifies modified packages only; do not run the monorepo suite here.\n- If no package-level code changes are present, mark this step skipped with a clear reason."
|
|
803
851
|
when /\Arelease(?:-.+)?\z/
|
|
804
852
|
"- Release all modified packages and update both package and root changelogs.\n- Follow semantic versioning expectations for this step.\n- When auto-detecting packages, include `git diff origin/main...HEAD --name-only` in addition to working-tree state — prior steps may have already committed changes."
|
|
805
|
-
when "verify-e2e"
|
|
806
|
-
"- Check change scope: run `git diff origin/main --name-only` to list modified files.\n" \
|
|
807
|
-
"- **Skip criteria**: If ALL modified files match `*.md`, `*.yml` (non-CI config), `.ace-tasks/**`, or `.ace-retros/**`, skip E2E verification — mark step done with \"skipped: docs/task-spec only changes, no runnable code affected\".\n" \
|
|
808
|
-
"- Otherwise: detect modified packages, run E2E scenarios for each package with `test/e2e/` scenarios#{task_hint}.\n" \
|
|
809
|
-
"- If no modified package has E2E scenarios, mark step done with \"skipped: no E2E scenarios for modified packages\"."
|
|
810
853
|
else
|
|
811
854
|
"- Execute the #{sub_name} step."
|
|
812
855
|
end
|
|
@@ -849,25 +892,52 @@ module Ace
|
|
|
849
892
|
"instructions" => rendered_instructions,
|
|
850
893
|
"workflow" => rendering["workflow"]
|
|
851
894
|
)
|
|
852
|
-
|
|
853
|
-
materialized["
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
895
|
+
resolved_source = resolved_step_source(step, rendering)
|
|
896
|
+
materialized["source"] = resolved_source if resolved_source && !resolved_source.empty?
|
|
897
|
+
unless split_child_without_explicit_fork?(step)
|
|
898
|
+
context_default = rendering.dig("context", "default")
|
|
899
|
+
materialized["context"] ||= context_default if context_default
|
|
900
|
+
fork_context = rendering.dig("context", "fork")
|
|
901
|
+
if materialized["context"] == "fork" && fork_context.is_a?(Hash) && !fork_context.empty?
|
|
902
|
+
# For materialized explicit steps, preserve frontmatter-provided fork config
|
|
903
|
+
# (`||=` semantics). Rendering contributes defaults only when the step
|
|
904
|
+
# itself did not declare fork options.
|
|
905
|
+
materialized["fork"] ||= fork_context
|
|
906
|
+
end
|
|
860
907
|
end
|
|
861
908
|
materialized["source_skill"] = rendering["source_skill"] || rendering["skill"] if rendering["source_skill"] || rendering["skill"]
|
|
862
909
|
materialized["source_workflow"] = rendering["workflow"] if rendering["workflow"] && !rendering["workflow"].empty?
|
|
863
|
-
materialized.delete("skill")
|
|
910
|
+
materialized.delete("skill") unless preserve_explicit_child_skill?(step)
|
|
864
911
|
materialized
|
|
865
912
|
end
|
|
866
913
|
|
|
867
914
|
def resolve_step_rendering(step)
|
|
915
|
+
explicit_source = step["source"]&.to_s&.strip
|
|
916
|
+
if explicit_source && !explicit_source.empty?
|
|
917
|
+
canonical_step = find_step_definition_with_source_fallback(step, explicit_source: explicit_source)
|
|
918
|
+
if canonical_step && split_child_without_explicit_fork?(step)
|
|
919
|
+
canonical_step = canonical_step.dup
|
|
920
|
+
canonical_step.delete("context")
|
|
921
|
+
canonical_step.delete("fork")
|
|
922
|
+
end
|
|
923
|
+
source_skill = step["source_skill"]&.to_s&.strip
|
|
924
|
+
source_skill = canonical_step&.dig("source_skill") if source_skill.nil? || source_skill.empty?
|
|
925
|
+
rendering = skill_source_resolver.resolve_source_rendering(
|
|
926
|
+
explicit_source,
|
|
927
|
+
step_name: step["name"]&.to_s,
|
|
928
|
+
source_skill: source_skill
|
|
929
|
+
)
|
|
930
|
+
return canonical_step ? canonical_step.merge(rendering || {}) : rendering if rendering
|
|
931
|
+
end
|
|
932
|
+
|
|
868
933
|
explicit_workflow = step["workflow"]&.to_s&.strip
|
|
869
934
|
if explicit_workflow && !explicit_workflow.empty?
|
|
870
935
|
canonical_step = find_step_definition(step["name"]&.to_s)
|
|
936
|
+
if canonical_step && split_child_without_explicit_fork?(step)
|
|
937
|
+
canonical_step = canonical_step.dup
|
|
938
|
+
canonical_step.delete("context")
|
|
939
|
+
canonical_step.delete("fork")
|
|
940
|
+
end
|
|
871
941
|
source_skill = step["source_skill"]&.to_s&.strip
|
|
872
942
|
source_skill = canonical_step&.dig("source_skill") if source_skill.nil? || source_skill.empty?
|
|
873
943
|
rendering = skill_source_resolver.resolve_workflow_rendering(
|
|
@@ -893,6 +963,20 @@ module Ace
|
|
|
893
963
|
skill_source_resolver.resolve_step_rendering(step["name"]&.to_s)
|
|
894
964
|
end
|
|
895
965
|
|
|
966
|
+
def split_child_without_explicit_fork?(step)
|
|
967
|
+
step["parent"] && !step.key?("context") && !step.key?("fork")
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def preserve_explicit_child_skill?(step)
|
|
971
|
+
step["parent"] && step["sub_steps_origin"] == "explicit"
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def fork_context_value?(value)
|
|
975
|
+
normalized = value.to_s.strip.downcase
|
|
976
|
+
normalized = normalized.delete_prefix(":")
|
|
977
|
+
normalized == "fork"
|
|
978
|
+
end
|
|
979
|
+
|
|
896
980
|
def render_skill_backed_step_instructions(step:, rendering:)
|
|
897
981
|
if step_render_mode(rendering) == "step_template"
|
|
898
982
|
return render_step_template_instructions(step: step, rendering: rendering)
|
|
@@ -1006,6 +1090,15 @@ module Ace
|
|
|
1006
1090
|
end
|
|
1007
1091
|
|
|
1008
1092
|
def resolve_step_assign_config(step)
|
|
1093
|
+
source_ref = step["source"]&.to_s&.strip
|
|
1094
|
+
if source_ref && !source_ref.empty?
|
|
1095
|
+
return skill_source_resolver.resolve_source_assign_config(
|
|
1096
|
+
source_ref,
|
|
1097
|
+
step_name: step["name"]&.to_s,
|
|
1098
|
+
source_skill: step["source_skill"]&.to_s
|
|
1099
|
+
)
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1009
1102
|
explicit_workflow = step["workflow"]&.to_s&.strip
|
|
1010
1103
|
if explicit_workflow && !explicit_workflow.empty?
|
|
1011
1104
|
return skill_source_resolver.resolve_workflow_assign_config(
|
|
@@ -1086,30 +1179,120 @@ module Ace
|
|
|
1086
1179
|
Atoms::CatalogLoader.find_by_name(step_catalog, step_name)
|
|
1087
1180
|
end
|
|
1088
1181
|
|
|
1182
|
+
def find_step_definition_with_source_fallback(step, explicit_source:)
|
|
1183
|
+
step_name = step["name"]&.to_s
|
|
1184
|
+
canonical_step = find_step_definition(step_name)
|
|
1185
|
+
return canonical_step if canonical_step
|
|
1186
|
+
|
|
1187
|
+
source = explicit_source.to_s.strip
|
|
1188
|
+
return nil if source.empty?
|
|
1189
|
+
|
|
1190
|
+
source_skill = step["source_skill"]&.to_s&.strip
|
|
1191
|
+
source_skill = source.delete_prefix("skill://").strip if source_skill.to_s.empty? && source.start_with?("skill://")
|
|
1192
|
+
|
|
1193
|
+
step_catalog.find do |entry|
|
|
1194
|
+
next unless entry.is_a?(Hash)
|
|
1195
|
+
|
|
1196
|
+
entry_source = entry["source"]&.to_s&.strip
|
|
1197
|
+
entry_workflow = entry["workflow"]&.to_s&.strip
|
|
1198
|
+
entry_source_skill = entry["source_skill"]&.to_s&.strip
|
|
1199
|
+
entry_skill = entry["skill"]&.to_s&.strip
|
|
1200
|
+
|
|
1201
|
+
next true if entry_source == source || entry_workflow == source
|
|
1202
|
+
next true if !source_skill.to_s.empty? && (entry_source_skill == source_skill || entry_skill == source_skill)
|
|
1203
|
+
|
|
1204
|
+
false
|
|
1205
|
+
end
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1089
1208
|
# Load step catalog from project override or gem defaults.
|
|
1090
1209
|
#
|
|
1091
1210
|
# @return [Array<Hash>] Loaded step definitions
|
|
1092
1211
|
def step_catalog
|
|
1093
|
-
@step_catalog
|
|
1094
|
-
project_root = Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
|
|
1095
|
-
gem_root = Gem.loaded_specs["ace-assign"]&.gem_dir || File.expand_path("../../../..", __dir__)
|
|
1212
|
+
return @step_catalog if @step_catalog_loaded
|
|
1096
1213
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1214
|
+
if @step_catalog_from_fixture_set
|
|
1215
|
+
@step_catalog_loaded = true
|
|
1216
|
+
@step_catalog = @step_catalog_from_fixture
|
|
1217
|
+
return @step_catalog
|
|
1218
|
+
end
|
|
1099
1219
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1220
|
+
cached = self.class.send(:cached_value, :step_catalog_cache, step_catalog_signature)
|
|
1221
|
+
return @step_catalog = cached if cached
|
|
1222
|
+
|
|
1223
|
+
@step_catalog_loaded = true
|
|
1224
|
+
@step_catalog = load_step_catalog
|
|
1225
|
+
self.class.send(:store_cached_value, :step_catalog_cache, step_catalog_signature, @step_catalog)
|
|
1226
|
+
@step_catalog
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def step_catalog_signature
|
|
1230
|
+
[
|
|
1231
|
+
PROJECT_ROOT_SIGNAL,
|
|
1232
|
+
project_catalog_signature,
|
|
1233
|
+
default_catalog_signature,
|
|
1234
|
+
step_catalog_cache_token,
|
|
1235
|
+
CATALOG_SIGNAL
|
|
1236
|
+
].join("|")
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
def project_catalog_signature
|
|
1240
|
+
@project_catalog_signature ||= catalog_signature(File.join(project_root, ".ace", "assign", "catalog", "steps"))
|
|
1241
|
+
end
|
|
1107
1242
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1243
|
+
def default_catalog_signature
|
|
1244
|
+
@default_catalog_signature ||= catalog_signature(File.join(gem_root, ".ace-defaults", "assign", "catalog", "steps"))
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
def load_step_catalog
|
|
1248
|
+
project_catalog = File.join(project_root, ".ace", "assign", "catalog", "steps")
|
|
1249
|
+
default_catalog = File.join(gem_root, ".ace-defaults", "assign", "catalog", "steps")
|
|
1250
|
+
|
|
1251
|
+
canonical_steps = @skill_source_resolver.assign_step_catalog
|
|
1252
|
+
default_steps = Atoms::CatalogLoader.load_all(default_catalog, canonical_steps: false)
|
|
1253
|
+
base_catalog = merge_step_catalog(default_steps, canonical_steps)
|
|
1254
|
+
|
|
1255
|
+
if File.directory?(project_catalog)
|
|
1256
|
+
project_steps = Atoms::CatalogLoader.load_all(project_catalog, canonical_steps: false)
|
|
1257
|
+
merge_step_catalog(base_catalog, project_steps)
|
|
1258
|
+
else
|
|
1259
|
+
base_catalog
|
|
1110
1260
|
end
|
|
1111
1261
|
end
|
|
1112
1262
|
|
|
1263
|
+
def catalog_signature(catalog_dir)
|
|
1264
|
+
return "missing" unless File.directory?(catalog_dir)
|
|
1265
|
+
|
|
1266
|
+
Dir.glob(File.join(catalog_dir, "*.step.yml")).sort.map do |path|
|
|
1267
|
+
"#{path}:#{file_signature(path)}"
|
|
1268
|
+
end.join("|")
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1271
|
+
def file_signature(path)
|
|
1272
|
+
stat = File.stat(path)
|
|
1273
|
+
"#{stat.mtime.to_f}:#{stat.size}"
|
|
1274
|
+
rescue
|
|
1275
|
+
"missing"
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
def step_catalog_cache_token
|
|
1279
|
+
token = if @skill_source_resolver.respond_to?(:cache_signature)
|
|
1280
|
+
@skill_source_resolver.cache_signature
|
|
1281
|
+
else
|
|
1282
|
+
"resolver:#{@skill_source_resolver.object_id}"
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
"resolver:#{token}"
|
|
1286
|
+
end
|
|
1287
|
+
|
|
1288
|
+
def project_root
|
|
1289
|
+
@project_root ||= Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
def gem_root
|
|
1293
|
+
@gem_root ||= Gem.loaded_specs["ace-assign"]&.gem_dir || File.expand_path("../../../..", __dir__)
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1113
1296
|
# Merge default and project step catalogs by step name.
|
|
1114
1297
|
# Later definitions override earlier ones with matching names.
|
|
1115
1298
|
#
|
|
@@ -1145,6 +1328,11 @@ module Ace
|
|
|
1145
1328
|
|
|
1146
1329
|
merged = base.dup
|
|
1147
1330
|
override.each do |key, value|
|
|
1331
|
+
if runtime_binding_override_key?(key, base, override)
|
|
1332
|
+
merged[key] = base[key]
|
|
1333
|
+
next
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1148
1336
|
merged[key] =
|
|
1149
1337
|
if merged[key].is_a?(Hash) && value.is_a?(Hash)
|
|
1150
1338
|
deep_merge_step_definition(merged[key], value)
|
|
@@ -1155,6 +1343,33 @@ module Ace
|
|
|
1155
1343
|
merged
|
|
1156
1344
|
end
|
|
1157
1345
|
|
|
1346
|
+
def runtime_binding_override_key?(key, base, override)
|
|
1347
|
+
return false unless %w[source workflow skill source_skill].include?(key)
|
|
1348
|
+
return false unless local_runtime_binding_present?(base)
|
|
1349
|
+
canonical_binding_present?(override)
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
def local_runtime_binding_present?(entry)
|
|
1353
|
+
entry.is_a?(Hash) && (
|
|
1354
|
+
present_string?(entry["source"]) ||
|
|
1355
|
+
present_string?(entry["workflow"]) ||
|
|
1356
|
+
present_string?(entry["skill"])
|
|
1357
|
+
)
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
def canonical_binding_present?(entry)
|
|
1361
|
+
entry.is_a?(Hash) && (
|
|
1362
|
+
present_string?(entry["source"]) ||
|
|
1363
|
+
present_string?(entry["workflow"]) ||
|
|
1364
|
+
present_string?(entry["skill"]) ||
|
|
1365
|
+
present_string?(entry["source_skill"])
|
|
1366
|
+
)
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
def present_string?(value)
|
|
1370
|
+
value.is_a?(String) && !value.strip.empty?
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1158
1373
|
# Archive source config into the task's jobs/ directory.
|
|
1159
1374
|
# If config is already in a jobs/ or steps/ directory, keeps it in place.
|
|
1160
1375
|
# Otherwise moves job.yaml to <task>/jobs/<assignment_id>-job.yml for provenance.
|
|
@@ -1201,6 +1416,7 @@ module Ace
|
|
|
1201
1416
|
|
|
1202
1417
|
def insert_batch_step_tree(step_config, after:, as_child:, added_by:, location:)
|
|
1203
1418
|
normalized = normalize_batch_step_hash(step_config, location: location)
|
|
1419
|
+
normalized = apply_inferred_parent_for_sibling_insert(normalized, after: after, as_child: as_child)
|
|
1204
1420
|
|
|
1205
1421
|
if canonical_batch_insert_requested?(normalized)
|
|
1206
1422
|
canonical_inserted = insert_canonical_batch_step_tree(
|
|
@@ -1247,10 +1463,27 @@ module Ace
|
|
|
1247
1463
|
def canonical_batch_insert_requested?(step_config)
|
|
1248
1464
|
raw_sub_steps = step_config["sub_steps"] || step_config["sub-steps"]
|
|
1249
1465
|
has_declared_sub_steps = raw_sub_steps.is_a?(Array) && raw_sub_steps.any?
|
|
1466
|
+
has_source = !step_config["source"].to_s.strip.empty?
|
|
1250
1467
|
has_workflow = !step_config["workflow"].to_s.strip.empty?
|
|
1251
1468
|
has_skill = !step_config["skill"].to_s.strip.empty?
|
|
1252
1469
|
|
|
1253
|
-
has_declared_sub_steps || has_workflow || has_skill
|
|
1470
|
+
has_declared_sub_steps || has_source || has_workflow || has_skill
|
|
1471
|
+
end
|
|
1472
|
+
|
|
1473
|
+
def resolved_step_source(step, rendering)
|
|
1474
|
+
explicit_source = step["source"]&.to_s&.strip
|
|
1475
|
+
return explicit_source unless explicit_source.nil? || explicit_source.empty?
|
|
1476
|
+
|
|
1477
|
+
rendered_source = rendering["source"]&.to_s&.strip
|
|
1478
|
+
return rendered_source unless rendered_source.nil? || rendered_source.empty?
|
|
1479
|
+
|
|
1480
|
+
workflow_source = rendering["workflow"]&.to_s&.strip
|
|
1481
|
+
return workflow_source unless workflow_source.nil? || workflow_source.empty?
|
|
1482
|
+
|
|
1483
|
+
skill_name = rendering["skill"]&.to_s&.strip
|
|
1484
|
+
return nil if skill_name.nil? || skill_name.empty?
|
|
1485
|
+
|
|
1486
|
+
"skill://#{skill_name}"
|
|
1254
1487
|
end
|
|
1255
1488
|
|
|
1256
1489
|
def insert_canonical_batch_step_tree(step_config, after:, as_child:, added_by:, location:)
|
|
@@ -1456,6 +1689,27 @@ module Ace
|
|
|
1456
1689
|
end
|
|
1457
1690
|
end
|
|
1458
1691
|
|
|
1692
|
+
def apply_inferred_parent_for_sibling_insert(step_config, after:, as_child:)
|
|
1693
|
+
return step_config unless step_config.is_a?(Hash)
|
|
1694
|
+
return step_config if as_child
|
|
1695
|
+
return step_config if after.nil? || after.to_s.strip.empty?
|
|
1696
|
+
return step_config if step_config.key?("parent")
|
|
1697
|
+
|
|
1698
|
+
inferred_parent = infer_parent_from_anchor(after)
|
|
1699
|
+
return step_config if inferred_parent.nil? || inferred_parent.to_s.strip.empty?
|
|
1700
|
+
|
|
1701
|
+
step_config.merge("parent" => inferred_parent)
|
|
1702
|
+
end
|
|
1703
|
+
|
|
1704
|
+
def infer_parent_from_anchor(anchor_number)
|
|
1705
|
+
assignment = assignment_manager.find_active
|
|
1706
|
+
return nil unless assignment
|
|
1707
|
+
|
|
1708
|
+
state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
|
|
1709
|
+
anchor = state.find_by_number(anchor_number.to_s.strip)
|
|
1710
|
+
anchor&.parent
|
|
1711
|
+
end
|
|
1712
|
+
|
|
1459
1713
|
def default_dynamic_step_instructions
|
|
1460
1714
|
DEFAULT_DYNAMIC_STEP_INSTRUCTIONS
|
|
1461
1715
|
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.53.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-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -99,14 +99,14 @@ dependencies:
|
|
|
99
99
|
requirements:
|
|
100
100
|
- - "~>"
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0.
|
|
102
|
+
version: '0.34'
|
|
103
103
|
type: :runtime
|
|
104
104
|
prerelease: false
|
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0.
|
|
109
|
+
version: '0.34'
|
|
110
110
|
- !ruby/object:Gem::Dependency
|
|
111
111
|
name: ace-task
|
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -206,7 +206,6 @@ files:
|
|
|
206
206
|
- ".ace-defaults/assign/catalog/steps/task-load.step.yml"
|
|
207
207
|
- ".ace-defaults/assign/catalog/steps/update-docs.step.yml"
|
|
208
208
|
- ".ace-defaults/assign/catalog/steps/update-pr-desc.step.yml"
|
|
209
|
-
- ".ace-defaults/assign/catalog/steps/verify-e2e.step.yml"
|
|
210
209
|
- ".ace-defaults/assign/catalog/steps/verify-test-suite.step.yml"
|
|
211
210
|
- ".ace-defaults/assign/catalog/steps/verify-test.step.yml"
|
|
212
211
|
- ".ace-defaults/assign/catalog/steps/work-on-task.step.yml"
|
|
@@ -221,6 +220,11 @@ files:
|
|
|
221
220
|
- CHANGELOG.md
|
|
222
221
|
- README.md
|
|
223
222
|
- Rakefile
|
|
223
|
+
- docs/demo/canonical-skill-source.gif
|
|
224
|
+
- docs/demo/canonical-skill-source.tape.yml
|
|
225
|
+
- docs/demo/fork-provider.cast
|
|
226
|
+
- docs/demo/fork-provider.gif
|
|
227
|
+
- docs/demo/fork-provider.recording.json
|
|
224
228
|
- docs/demo/fork-provider.tape.yml
|
|
225
229
|
- docs/exit-codes.md
|
|
226
230
|
- docs/getting-started.md
|
|
@@ -236,14 +240,23 @@ files:
|
|
|
236
240
|
- handbook/skills/as-assign-recover-fork/SKILL.md
|
|
237
241
|
- handbook/skills/as-assign-run-in-batches/SKILL.md
|
|
238
242
|
- handbook/skills/as-assign-start/SKILL.md
|
|
243
|
+
- handbook/skills/as-create-retro-internal/SKILL.md
|
|
244
|
+
- handbook/skills/as-mark-task-done-internal/SKILL.md
|
|
245
|
+
- handbook/skills/as-reflect-and-refactor-internal/SKILL.md
|
|
246
|
+
- handbook/skills/as-task-load-internal/SKILL.md
|
|
239
247
|
- handbook/workflow-instructions/assign/add-task.wf.md
|
|
240
248
|
- handbook/workflow-instructions/assign/compose.wf.md
|
|
249
|
+
- handbook/workflow-instructions/assign/create-retro-internal.wf.md
|
|
241
250
|
- handbook/workflow-instructions/assign/create.wf.md
|
|
242
251
|
- handbook/workflow-instructions/assign/drive.wf.md
|
|
252
|
+
- handbook/workflow-instructions/assign/mark-task-done-internal.wf.md
|
|
243
253
|
- handbook/workflow-instructions/assign/prepare.wf.md
|
|
244
254
|
- handbook/workflow-instructions/assign/recover-fork.wf.md
|
|
255
|
+
- handbook/workflow-instructions/assign/reflect-and-refactor-internal.wf.md
|
|
245
256
|
- handbook/workflow-instructions/assign/run-in-batches.wf.md
|
|
246
257
|
- handbook/workflow-instructions/assign/start.wf.md
|
|
258
|
+
- handbook/workflow-instructions/assign/task-load-internal.wf.md
|
|
259
|
+
- handbook/workflow-instructions/assign/verify-test-suite.wf.md
|
|
247
260
|
- lib/ace/assign.rb
|
|
248
261
|
- lib/ace/assign/atoms/assign_frontmatter_parser.rb
|
|
249
262
|
- lib/ace/assign/atoms/catalog_loader.rb
|
|
@@ -263,11 +276,13 @@ files:
|
|
|
263
276
|
- lib/ace/assign/cli/commands/fail.rb
|
|
264
277
|
- lib/ace/assign/cli/commands/finish.rb
|
|
265
278
|
- lib/ace/assign/cli/commands/fork_run.rb
|
|
279
|
+
- lib/ace/assign/cli/commands/fork_session.rb
|
|
266
280
|
- lib/ace/assign/cli/commands/list.rb
|
|
267
281
|
- lib/ace/assign/cli/commands/retry_cmd.rb
|
|
268
282
|
- lib/ace/assign/cli/commands/select.rb
|
|
269
283
|
- lib/ace/assign/cli/commands/start.rb
|
|
270
284
|
- lib/ace/assign/cli/commands/status.rb
|
|
285
|
+
- lib/ace/assign/cli/commands/step.rb
|
|
271
286
|
- lib/ace/assign/models/assignment.rb
|
|
272
287
|
- lib/ace/assign/models/assignment_info.rb
|
|
273
288
|
- lib/ace/assign/models/queue_state.rb
|
|
@@ -280,6 +295,7 @@ files:
|
|
|
280
295
|
- lib/ace/assign/molecules/skill_assign_source_resolver.rb
|
|
281
296
|
- lib/ace/assign/molecules/step_renumberer.rb
|
|
282
297
|
- lib/ace/assign/molecules/step_writer.rb
|
|
298
|
+
- lib/ace/assign/molecules/tmux_fork_runner.rb
|
|
283
299
|
- lib/ace/assign/organisms/assignment_executor.rb
|
|
284
300
|
- lib/ace/assign/organisms/task_assignment_creator.rb
|
|
285
301
|
- lib/ace/assign/version.rb
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
name: verify-e2e
|
|
2
|
-
skill: as-e2e-review
|
|
3
|
-
render: step_template
|
|
4
|
-
description: Review E2E coverage for modified packages and run targeted scenarios
|
|
5
|
-
|
|
6
|
-
prerequisites:
|
|
7
|
-
- name: work-on-task
|
|
8
|
-
strength: required
|
|
9
|
-
reason: "Must have implementation to test"
|
|
10
|
-
- name: verify-test-suite
|
|
11
|
-
strength: recommended
|
|
12
|
-
reason: "Unit tests should pass before running E2E"
|
|
13
|
-
|
|
14
|
-
produces: [e2e-results, coverage-matrix]
|
|
15
|
-
consumes: [code-changes]
|
|
16
|
-
|
|
17
|
-
context:
|
|
18
|
-
default: null
|
|
19
|
-
reason: "E2E review and execution run in the project environment"
|
|
20
|
-
|
|
21
|
-
when_to_skip:
|
|
22
|
-
- "No public CLI API changes (internal-only refactoring)"
|
|
23
|
-
- "Package has no E2E test scenarios and no coverage gaps identified"
|
|
24
|
-
- "All modified files are docs-only (*.md, *.yml non-config, task specs, retros)"
|
|
25
|
-
|
|
26
|
-
effort: medium
|
|
27
|
-
tags: [testing, e2e, verification]
|
|
28
|
-
|
|
29
|
-
steps:
|
|
30
|
-
- name: review-coverage
|
|
31
|
-
description: "Run /as-e2e-review for each heavily modified package to get coverage matrix"
|
|
32
|
-
tool: "ace-e2e-review <package>"
|
|
33
|
-
note: "Identify gaps, overlaps, and which TCs need updating before running"
|
|
34
|
-
|
|
35
|
-
- name: update-if-needed
|
|
36
|
-
description: "If coverage matrix shows gaps or stale TCs, update or create E2E tests"
|
|
37
|
-
conditional: "coverage-matrix shows gaps or outdated TCs"
|
|
38
|
-
|
|
39
|
-
- name: run-targeted
|
|
40
|
-
description: "Run ace-test-e2e-suite for each heavily modified package (not full suite)"
|
|
41
|
-
tool: "ace-test-e2e-suite <package>"
|
|
42
|
-
note: "Pass package name(s) of heavily modified packages only"
|