ace-assign 0.37.0 → 0.40.3

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.
@@ -4,21 +4,33 @@ module Ace
4
4
  module Assign
5
5
  module CLI
6
6
  module Commands
7
- # Create a new workflow assignment from config file
7
+ # Create a new workflow assignment from YAML or preset-backed task refs
8
8
  class Create < Ace::Support::Cli::Command
9
9
  include Ace::Support::Cli::Base
10
10
 
11
- desc "Create a new workflow assignment from YAML config"
11
+ desc "Create a new workflow assignment"
12
12
 
13
- argument :config, required: true, desc: "Path to job.yaml config file"
13
+ option :yaml, desc: "Path to job.yaml config file"
14
+ option :task, aliases: ["-t"], type: :array,
15
+ desc: "Task reference(s), repeatable and comma-separated"
16
+ option :preset, aliases: ["-p"], desc: "Assignment preset name (task mode only)"
14
17
  option :quiet, aliases: ["-q"], type: :boolean, default: false, desc: "Suppress non-essential output"
15
18
  option :debug, aliases: ["-d"], type: :boolean, default: false, desc: "Show debug output"
16
19
 
17
- def call(config:, **options)
18
- executor = Organisms::AssignmentExecutor.new
19
- result = executor.start(config)
20
+ def call(yaml: nil, task: nil, preset: nil, **options)
21
+ validate_modes!(yaml, task, preset)
22
+
23
+ result = if yaml
24
+ Organisms::AssignmentExecutor.new.start(yaml)
25
+ else
26
+ Organisms::TaskAssignmentCreator.new.call(
27
+ task_refs: task,
28
+ preset_name: preset || Organisms::TaskAssignmentCreator::DEFAULT_PRESET
29
+ )
30
+ end
20
31
 
21
32
  unless options[:quiet]
33
+ print_terminal_skip_summary(result[:skipped_terminal])
22
34
  print_assignment_header(result[:assignment])
23
35
  print_step_instructions(result[:current])
24
36
  end
@@ -26,6 +38,23 @@ module Ace
26
38
 
27
39
  private
28
40
 
41
+ def validate_modes!(yaml, task, preset)
42
+ task_refs = Array(task).flat_map { |entry| entry.to_s.split(",") }.map(&:strip).reject(&:empty?)
43
+ selected = 0
44
+ selected += 1 if yaml && !yaml.to_s.strip.empty?
45
+ selected += 1 if task_refs.any?
46
+
47
+ raise Ace::Support::Cli::Error, "Exactly one of --yaml or --task is required" if selected.zero?
48
+ raise Ace::Support::Cli::Error, "--yaml and --task are mutually exclusive" if selected > 1
49
+ raise Ace::Support::Cli::Error, "--preset requires --task" if preset && task_refs.empty?
50
+ end
51
+
52
+ def print_terminal_skip_summary(skipped_terminal)
53
+ return if skipped_terminal.nil? || skipped_terminal.empty?
54
+
55
+ puts "Skipped terminal tasks (done/skipped/cancelled): #{skipped_terminal.join(', ')}"
56
+ end
57
+
29
58
  def print_assignment_header(assignment)
30
59
  puts "Assignment: #{assignment.name} (#{assignment.id})"
31
60
  puts "Created: #{display_path(assignment.cache_dir)}/"
@@ -14,6 +14,8 @@ require_relative "models/assignment_info"
14
14
  require_relative "atoms/step_numbering"
15
15
  require_relative "atoms/number_generator"
16
16
  require_relative "atoms/preset_expander"
17
+ require_relative "atoms/preset_loader"
18
+ require_relative "atoms/preset_step_resolver"
17
19
  require_relative "atoms/step_file_parser"
18
20
  require_relative "atoms/step_sorter"
19
21
  require_relative "atoms/catalog_loader"
@@ -29,6 +31,7 @@ require_relative "molecules/step_writer"
29
31
  require_relative "molecules/step_renumberer"
30
32
  require_relative "molecules/skill_assign_source_resolver"
31
33
  require_relative "molecules/fork_session_launcher"
34
+ require_relative "molecules/preset_inferrer"
32
35
 
33
36
  # Organisms
34
37
  require_relative "organisms/assignment_executor"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Ace
6
+ module Assign
7
+ module Molecules
8
+ # Infers assignment preset from archived source config.
9
+ module PresetInferrer
10
+ DEFAULT_PRESET = "work-on-task"
11
+
12
+ def self.infer_from_assignment(assignment)
13
+ return DEFAULT_PRESET unless assignment
14
+
15
+ source_path = assignment.source_config.to_s
16
+ return DEFAULT_PRESET if source_path.empty? || !File.exist?(source_path)
17
+
18
+ data = YAML.safe_load_file(source_path, aliases: true)
19
+ return DEFAULT_PRESET unless data.is_a?(Hash)
20
+
21
+ session_name = data.dig("session", "name").to_s.strip
22
+ return session_name unless session_name.empty?
23
+
24
+ DEFAULT_PRESET
25
+ rescue Psych::SyntaxError, Errno::ENOENT
26
+ DEFAULT_PRESET
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -11,6 +11,8 @@ module Ace
11
11
  # Implements the state machine for queue operations:
12
12
  # start → advance → complete (with fail/add/retry branches)
13
13
  class AssignmentExecutor
14
+ DEFAULT_DYNAMIC_STEP_INSTRUCTIONS = "Complete this step and finish with: ace-assign finish --message report.md".freeze
15
+
14
16
  attr_reader :assignment_manager, :queue_scanner, :step_writer, :step_renumberer, :skill_source_resolver
15
17
 
16
18
  def initialize(cache_base: nil)
@@ -105,7 +107,7 @@ module Ace
105
107
  # @return [Hash] Result with assignment and state
106
108
  def status
107
109
  assignment = assignment_manager.find_active
108
- raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create <job.yaml>' to begin." unless assignment
110
+ raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create --yaml <job.yaml>' to begin." unless assignment
109
111
 
110
112
  state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
111
113
  {
@@ -127,7 +129,7 @@ module Ace
127
129
  # @return [Hash] Result with started step and updated state
128
130
  def start_step(step_number: nil, fork_root: nil)
129
131
  assignment = assignment_manager.find_active
130
- raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create <job.yaml>' to begin." unless assignment
132
+ raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create --yaml <job.yaml>' to begin." unless assignment
131
133
 
132
134
  state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
133
135
  raise StepErrors::InvalidState, "Cannot start: step #{state.current.number} is already in progress. Finish or fail it first." if state.current
@@ -169,7 +171,7 @@ module Ace
169
171
  # @return [Hash] Result with completed step and updated state
170
172
  def finish_step(report_content:, step_number: nil, fork_root: nil)
171
173
  assignment = assignment_manager.find_active
172
- raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create <job.yaml>' to begin." unless assignment
174
+ raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create --yaml <job.yaml>' to begin." unless assignment
173
175
 
174
176
  state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
175
177
  current = find_target_step_for_finish(state, step_number, fork_root)
@@ -260,7 +262,7 @@ module Ace
260
262
  # @return [Hash] Result with updated state
261
263
  def fail(message)
262
264
  assignment = assignment_manager.find_active
263
- raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create <job.yaml>' to begin." unless assignment
265
+ raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create --yaml <job.yaml>' to begin." unless assignment
264
266
 
265
267
  state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
266
268
  current = state.current
@@ -288,9 +290,12 @@ module Ace
288
290
  # @param after [String, nil] Insert after this step number (optional)
289
291
  # @param as_child [Boolean] Insert as child of 'after' step (default: false, sibling)
290
292
  # @return [Hash] Result with new step
291
- def add(name, instructions, after: nil, as_child: false)
293
+ def add(name, instructions, after: nil, as_child: false, added_by: nil, extra: {})
292
294
  assignment = assignment_manager.find_active
293
- raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create <job.yaml>' to begin." unless assignment
295
+ raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create --yaml <job.yaml>' to begin." unless assignment
296
+
297
+ step_name = name.to_s.strip
298
+ raise Error, "Step name cannot be empty." if step_name.empty?
294
299
 
295
300
  state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
296
301
  existing_numbers = queue_scanner.step_numbers(assignment.steps_dir)
@@ -318,7 +323,7 @@ module Ace
318
323
  initial_status = state.current ? :pending : :in_progress
319
324
 
320
325
  # Build added_by metadata for audit trail
321
- added_by = if after && as_child
326
+ added_by ||= if after && as_child
322
327
  "child_of:#{after}"
323
328
  elsif after
324
329
  "injected_after:#{after}"
@@ -326,15 +331,18 @@ module Ace
326
331
  "dynamic"
327
332
  end
328
333
 
334
+ extra_frontmatter = normalize_batch_extra_fields(extra)
335
+
329
336
  # Create new step file with correct status
330
337
  step_writer.create(
331
338
  steps_dir: assignment.steps_dir,
332
339
  number: new_number,
333
- name: name,
340
+ name: step_name,
334
341
  instructions: instructions,
335
342
  status: initial_status,
336
343
  added_by: added_by,
337
- parent: as_child ? after : nil
344
+ parent: as_child ? after : nil,
345
+ extra: extra_frontmatter
338
346
  )
339
347
 
340
348
  rebalance_after_child_injection(assignment: assignment, state: state, parent_number: after) if as_child && after
@@ -354,13 +362,64 @@ module Ace
354
362
  }
355
363
  end
356
364
 
365
+ # Add multiple steps dynamically from a pre-parsed steps array.
366
+ #
367
+ # @param steps [Array<Hash>] Step definitions loaded from YAML
368
+ # @param after [String, nil] Insert after this step number
369
+ # @param as_child [Boolean] Insert as children of +after+
370
+ # @param source_file [String, nil] Source YAML path (for added_by audit metadata)
371
+ # @note Structural validation is performed for the full batch before any writes.
372
+ # Runtime I/O failures can still interrupt insertion after partial writes.
373
+ # @return [Hash] Result with added steps and final state
374
+ def add_batch(steps:, after: nil, as_child: false, source_file: nil)
375
+ unless steps.is_a?(Array) && steps.any?
376
+ source_label = source_file.to_s.strip.empty? ? "batch input" : source_file
377
+ raise Error, "No steps defined in #{source_label}"
378
+ end
379
+
380
+ if as_child && (after.nil? || after.to_s.strip.empty?)
381
+ raise Error, "Child insertion requires an after step reference."
382
+ end
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
+ prevalidate_batch_trees!(steps)
388
+
389
+ added_steps = []
390
+ renumbered = []
391
+ sibling_cursor = after
392
+
393
+ steps.each_with_index do |step_config, index|
394
+ inserted = insert_batch_step_tree(
395
+ step_config,
396
+ after: as_child ? after : sibling_cursor,
397
+ as_child: as_child,
398
+ added_by: batch_added_by,
399
+ location: "steps[#{index}]"
400
+ )
401
+ added_steps.concat(inserted[:added])
402
+ renumbered.concat(inserted[:renumbered])
403
+ sibling_cursor = inserted[:root_number] unless as_child
404
+ end
405
+
406
+ assignment = assignment_manager.find_active
407
+ state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
408
+ {
409
+ assignment: assignment,
410
+ state: state,
411
+ added: added_steps,
412
+ renumbered: renumbered.uniq
413
+ }
414
+ end
415
+
357
416
  # Retry a failed step (creates new step linked to original)
358
417
  #
359
418
  # @param step_ref [String] Step number or reference to retry
360
419
  # @return [Hash] Result with new retry step
361
420
  def retry_step(step_ref)
362
421
  assignment = assignment_manager.find_active
363
- raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create <job.yaml>' to begin." unless assignment
422
+ raise AssignmentErrors::NoActive, "No active assignment. Use 'ace-assign create --yaml <job.yaml>' to begin." unless assignment
364
423
 
365
424
  state = queue_scanner.scan(assignment.steps_dir, assignment: assignment)
366
425
 
@@ -1119,6 +1178,267 @@ module Ace
1119
1178
  instructions.is_a?(Array) ? instructions.join("\n") : instructions.to_s
1120
1179
  end
1121
1180
 
1181
+ def insert_batch_step_tree(step_config, after:, as_child:, added_by:, location:)
1182
+ normalized = normalize_batch_step_hash(step_config, location: location)
1183
+
1184
+ if canonical_batch_insert_requested?(normalized)
1185
+ canonical_inserted = insert_canonical_batch_step_tree(
1186
+ normalized,
1187
+ after: after,
1188
+ as_child: as_child,
1189
+ added_by: added_by,
1190
+ location: location
1191
+ )
1192
+ return canonical_inserted if canonical_inserted
1193
+ end
1194
+
1195
+ prepared = materialize_batch_step_config(normalized)
1196
+ instructions = normalize_instructions(prepared["instructions"])
1197
+
1198
+ result = add(
1199
+ prepared["name"],
1200
+ instructions,
1201
+ after: after,
1202
+ as_child: as_child,
1203
+ added_by: added_by,
1204
+ extra: prepared
1205
+ )
1206
+
1207
+ root_step = result[:added]
1208
+ added_steps = [root_step]
1209
+ renumbered = Array(result[:renumbered])
1210
+
1211
+ normalize_batch_sub_steps(prepared, location: location).each_with_index do |child_config, index|
1212
+ child_inserted = insert_batch_step_tree(
1213
+ child_config,
1214
+ after: root_step.number,
1215
+ as_child: true,
1216
+ added_by: added_by,
1217
+ location: "#{location}.sub_steps[#{index}]"
1218
+ )
1219
+ added_steps.concat(child_inserted[:added])
1220
+ renumbered.concat(child_inserted[:renumbered])
1221
+ end
1222
+
1223
+ {added: added_steps, renumbered: renumbered, root_number: root_step.number}
1224
+ end
1225
+
1226
+ def canonical_batch_insert_requested?(step_config)
1227
+ raw_sub_steps = step_config["sub_steps"] || step_config["sub-steps"]
1228
+ has_declared_sub_steps = raw_sub_steps.is_a?(Array) && raw_sub_steps.any?
1229
+ has_workflow = !step_config["workflow"].to_s.strip.empty?
1230
+ has_skill = !step_config["skill"].to_s.strip.empty?
1231
+
1232
+ has_declared_sub_steps || has_workflow || has_skill
1233
+ end
1234
+
1235
+ def insert_canonical_batch_step_tree(step_config, after:, as_child:, added_by:, location:)
1236
+ materialized_tree = materialize_canonical_batch_tree(step_config, location: location)
1237
+ return nil if materialized_tree.nil? || materialized_tree.empty?
1238
+
1239
+ root_template = materialized_tree.find { |step| step["parent"].nil? } || materialized_tree.first
1240
+ root_instructions = normalize_instructions(root_template["instructions"])
1241
+ root_instructions = default_dynamic_step_instructions if root_instructions.strip.empty?
1242
+
1243
+ root_result = add(
1244
+ root_template["name"],
1245
+ root_instructions,
1246
+ after: after,
1247
+ as_child: as_child,
1248
+ added_by: added_by,
1249
+ extra: root_template
1250
+ )
1251
+
1252
+ root_step = root_result[:added]
1253
+ added_steps = [root_step]
1254
+ renumbered = Array(root_result[:renumbered])
1255
+
1256
+ root_number = root_template["number"]
1257
+ children = materialized_tree
1258
+ .select { |step| step["parent"] == root_number }
1259
+ .sort_by { |step| step["number"].to_s }
1260
+
1261
+ children.each do |child_template|
1262
+ child_instructions = normalize_instructions(child_template["instructions"])
1263
+ child_instructions = default_dynamic_step_instructions if child_instructions.strip.empty?
1264
+ child_result = add(
1265
+ child_template["name"],
1266
+ child_instructions,
1267
+ after: root_step.number,
1268
+ as_child: true,
1269
+ added_by: added_by,
1270
+ extra: child_template
1271
+ )
1272
+
1273
+ added_steps << child_result[:added]
1274
+ renumbered.concat(Array(child_result[:renumbered]))
1275
+ end
1276
+
1277
+ {added: added_steps, renumbered: renumbered, root_number: root_step.number}
1278
+ end
1279
+
1280
+ def materialize_canonical_batch_tree(step_config, location:)
1281
+ canonical_input, child_overrides = prepare_canonical_batch_input(step_config, location: location)
1282
+ return nil unless canonical_input
1283
+
1284
+ expanded = expand_sub_steps([canonical_input])
1285
+ expanded = apply_canonical_child_overrides(expanded, child_overrides)
1286
+ materialize_skill_backed_steps(expanded)
1287
+ end
1288
+
1289
+ def prepare_canonical_batch_input(step_config, location:)
1290
+ enriched = enrich_declared_sub_steps([step_config]).first
1291
+ raw_sub_steps = enriched["sub_steps"] || enriched["sub-steps"]
1292
+ descriptors = parse_canonical_sub_step_descriptors(raw_sub_steps, location: "#{location}.sub_steps")
1293
+ return [nil, {}] if descriptors.nil?
1294
+
1295
+ names = descriptors[:names]
1296
+ overrides = descriptors[:overrides]
1297
+ canonical = enriched.dup
1298
+ if names
1299
+ canonical["sub_steps"] = names
1300
+ canonical.delete("sub-steps")
1301
+ end
1302
+
1303
+ [canonical, overrides]
1304
+ end
1305
+
1306
+ def parse_canonical_sub_step_descriptors(raw_sub_steps, location:)
1307
+ return {names: nil, overrides: {}} if raw_sub_steps.nil?
1308
+ return nil unless raw_sub_steps.is_a?(Array)
1309
+
1310
+ names = []
1311
+ overrides = {}
1312
+
1313
+ raw_sub_steps.each_with_index do |entry, index|
1314
+ case entry
1315
+ when String
1316
+ name = entry.to_s.strip
1317
+ raise Error, "sub_steps entry at #{location}[#{index}] cannot be empty" if name.empty?
1318
+
1319
+ names << name
1320
+ when Hash
1321
+ normalized = normalize_batch_step_hash(entry, location: "#{location}[#{index}]")
1322
+ return nil if normalized.key?("sub_steps") || normalized.key?("sub-steps")
1323
+
1324
+ names << normalized["name"]
1325
+ overrides[index] = normalized
1326
+ else
1327
+ return nil
1328
+ end
1329
+ end
1330
+
1331
+ {names: names, overrides: overrides}
1332
+ end
1333
+
1334
+ def apply_canonical_child_overrides(expanded_steps, overrides)
1335
+ return expanded_steps if overrides.empty?
1336
+
1337
+ root = expanded_steps.find { |step| step["parent"].nil? } || expanded_steps.first
1338
+ root_number = root["number"]
1339
+ children = expanded_steps
1340
+ .select { |step| step["parent"] == root_number }
1341
+ .sort_by { |step| step["number"].to_s }
1342
+
1343
+ merged_children = children.each_with_index.map do |child, index|
1344
+ override = overrides[index]
1345
+ next child unless override
1346
+
1347
+ child.merge(override).merge(
1348
+ "number" => child["number"],
1349
+ "parent" => child["parent"]
1350
+ )
1351
+ end
1352
+
1353
+ [root] + merged_children
1354
+ end
1355
+
1356
+ def materialize_batch_step_config(step_config)
1357
+ prepared = materialize_skill_backed_step(step_config)
1358
+ instructions = normalize_instructions(prepared["instructions"])
1359
+ prepared["instructions"] = default_dynamic_step_instructions if instructions.strip.empty?
1360
+ prepared
1361
+ end
1362
+
1363
+ def prevalidate_batch_trees!(steps)
1364
+ steps.each_with_index do |step_config, index|
1365
+ prevalidate_batch_step_tree(step_config, location: "steps[#{index}]")
1366
+ end
1367
+ end
1368
+
1369
+ def prevalidate_batch_step_tree(step_config, location:)
1370
+ normalized = normalize_batch_step_hash(step_config, location: location)
1371
+
1372
+ if canonical_batch_insert_requested?(normalized)
1373
+ materialized_tree = materialize_canonical_batch_tree(normalized, location: location)
1374
+ return if materialized_tree
1375
+ end
1376
+
1377
+ prepared = materialize_batch_step_config(normalized)
1378
+ normalize_batch_sub_steps(prepared, location: location).each_with_index do |child_config, index|
1379
+ prevalidate_batch_step_tree(child_config, location: "#{location}.sub_steps[#{index}]")
1380
+ end
1381
+ end
1382
+
1383
+ def normalize_batch_step_hash(step_config, location:)
1384
+ unless step_config.is_a?(Hash)
1385
+ raise Error, "Invalid step definition at #{location}: expected mapping"
1386
+ end
1387
+
1388
+ normalized = step_config.each_with_object({}) do |(key, value), memo|
1389
+ memo[key.to_s] = value
1390
+ end
1391
+
1392
+ name = normalized["name"].to_s.strip
1393
+ raise Error, "Step name is required at #{location}" if name.empty?
1394
+
1395
+ normalized["name"] = name
1396
+ normalized
1397
+ end
1398
+
1399
+ def normalize_batch_sub_steps(step_config, location:)
1400
+ raw = step_config["sub_steps"] || step_config["sub-steps"]
1401
+ return [] unless raw
1402
+
1403
+ unless raw.is_a?(Array)
1404
+ raise Error, "sub_steps must be an array at #{location}"
1405
+ end
1406
+
1407
+ raw.each_with_index.map do |entry, index|
1408
+ case entry
1409
+ when String
1410
+ name = entry.to_s.strip
1411
+ raise Error, "sub_steps entry at #{location}[#{index}] cannot be empty" if name.empty?
1412
+
1413
+ {
1414
+ "name" => name,
1415
+ "instructions" => "Execute #{name} step."
1416
+ }
1417
+ when Hash
1418
+ normalized = normalize_batch_step_hash(entry, location: "#{location}[#{index}]")
1419
+ materialize_batch_step_config(normalized)
1420
+ else
1421
+ raise Error, "Invalid sub_steps entry at #{location}[#{index}]: expected string or mapping"
1422
+ end
1423
+ end
1424
+ end
1425
+
1426
+ def normalize_batch_extra_fields(step_config)
1427
+ return {} unless step_config.is_a?(Hash)
1428
+ return {} if step_config.empty?
1429
+
1430
+ reserved_keys = %w[name instructions number status parent added_by sub_steps sub-steps]
1431
+ step_config.each_with_object({}) do |(key, value), memo|
1432
+ key_str = key.to_s
1433
+ next if reserved_keys.include?(key_str)
1434
+ memo[key_str] = value
1435
+ end
1436
+ end
1437
+
1438
+ def default_dynamic_step_instructions
1439
+ DEFAULT_DYNAMIC_STEP_INSTRUCTIONS
1440
+ end
1441
+
1122
1442
  def find_target_step_for_start(state, step_number, fork_root)
1123
1443
  target = state.find_by_number(step_number)
1124
1444
  raise StepErrors::NotFound, "Step #{step_number} not found in queue" unless target
@@ -1264,7 +1584,11 @@ module Ace
1264
1584
  if after
1265
1585
  if as_child
1266
1586
  # Insert as first child of 'after'
1267
- new_number = Atoms::StepNumbering.next_child(after, existing_numbers)
1587
+ begin
1588
+ new_number = Atoms::StepNumbering.next_child(after, existing_numbers)
1589
+ rescue ArgumentError => e
1590
+ raise Error, e.message
1591
+ end
1268
1592
  [new_number, []]
1269
1593
  else
1270
1594
  # Insert as sibling after 'after'