The Assembly Doctor
The Assembly Doctor: syntax_suggest for plans. When a 12-step plan won't assemble, "KeyError: task not found" is technically true the way "syntax error, unexpected end" is technically true - it names the symptom and hides the street address. The doctor examines a broken plan spec the way syntax_suggest examines a broken file: find the smallest region that explains the failure, SHOW it with an arrow pointing at the problem, and suggest the one-keystroke fix when the framework …
Developer Experience
Round 17
Richard Schneeman
exit 0
bundle exec ruby examples/assembly_doctor.rb
a real captured run
THE ASSEMBLY DOCTOR (error messages are the UI of failure)
examining the 5pm deploy plan (8 steps): 2 wounds found
Unmatched dependency, in step 5:
4 "fetch metadata"
> 5 "upload assets" after: "fetch metdata"
^^^^^^^^^^^^^^^ no step has this name (did you mean fetch metadata?)
Dependency cycle - the plan can never start:
> run migrations -> verify health -> restart app -> run migrations
every member of the loop waits for another member. one of
these edges is aspirational, not structural - probably the
newest one.
after the two one-line fixes: doctor finds nothing; plan runs: completed
the doctor's rules are syntax_suggest's rules, one abstraction up:
don't report the symptom's location, report the SMALLEST REGION
that explains it (the typo'd edge, the whole loop); show the code,
not a stack trace; and when the system is holding the list of
valid names - it always is - spend one Levenshtein pass to turn
the diagnosis into a one-keystroke fix. the framework's own
Suggestions module supplied the did-you-mean. people don't quit
because programming is hard; they quit because failure is rude.
source
# frozen_string_literal: true # The Assembly Doctor: syntax_suggest for plans. When a 12-step plan # won't assemble, "KeyError: task not found" is technically true the # way "syntax error, unexpected end" is technically true - it names # the symptom and hides the street address. The doctor examines a # broken plan spec the way syntax_suggest examines a broken file: # find the smallest region that explains the failure, SHOW it with # an arrow pointing at the problem, and suggest the one-keystroke # fix when the framework can see it. Error messages are the UI of # failure, and failure is most of programming. # # bundle exec ruby examples/assembly_doctor.rb # # Runs offline; diagnoses two classic assembly wounds, then proves # the repaired plan actually runs. require class="s">"bundler/setup" require class="s">"agentic" Agentic.logger.level = class="y">:fatal # A deploy pipeline, written by a human at 5pm: one typo'd dependency # name, and one cycle added during a hasty refactor PLAN = [ {step: class="s">"checkout code"}, {step: class="s">"install deps", after: [class="s">"checkout code"]}, {step: class="s">"compile assets", after: [class="s">"install deps"]}, {step: class="s">"fetch metadata", after: [class="s">"checkout code"]}, {step: class="s">"upload assets", after: [class="s">"compile assets", class="s">"fetch metdata"]}, # <- 5pm {step: class="s">"run migrations", after: [class="s">"install deps", class="s">"verify health"]}, # <- the refactor {step: class="s">"restart app", after: [class="s">"run migrations", class="s">"upload assets"]}, {step: class="s">"verify health", after: [class="s">"restart app"]} ].freeze # --- the doctor ------------------------------------------------------------------- def diagnose(plan) names = plan.map { |s| s[class="y">:step] } findings = [] # Wound 1: dependencies that name no step (with the framework's own # did-you-mean pass - the plan is holding the list of valid names) plan.each_with_index do |step, i| Array(step[class="y">:after]).each do |dep| next if names.include?(dep) findings << {kind: class="s">"unmatched dependency", snippet: [i, dep], hint: Agentic:class="y">:Suggestions.hint(dep, names)} end end # Wound 2: cycles - shown as the LOOP itself, not as a deadlocked # hang. Depth-first search finds a member; walking dependencies that # still reach the origin reconstructs the loop for display deps_of = ->(name) { Array(plan.find { |s| s[class="y">:step] == name }&.dig(class="y">:after)) } reaches = ->(from, target, seen) { return false if seen.include?(from) deps_of.call(from).include?(target) || deps_of.call(from).any? { |d| reaches.call(d, target, seen + [from]) } } cycle_member = names.find { |n| reaches.call(n, n, []) } if cycle_member loop_path = [cycle_member] until loop_path.size > 1 && loop_path.last == cycle_member loop_path << deps_of.call(loop_path.last).find { |d| d == cycle_member || reaches.call(d, cycle_member, []) } end findings << {kind: class="s">"dependency cycle", snippet: loop_path} end findings end def print_diagnosis(plan, findings) findings.each do |f| case f[class="y">:kind] when class="s">"unmatched dependency" i, dep = f[class="y">:snippet] line = class="s">" > #{i + 1} #{plan[i][class="y">:step].inspect} after: #{dep.inspect}" puts class="s">" Unmatched dependency, in step #{i + 1}:" puts class="s">" #{i} #{plan[i - 1][class="y">:step].inspect}" puts line puts class="s">"#{" class="s">" * line.index(dep.inspect)}#{"^class="s">" * dep.inspect.size} no step has this name#{f[class="y">:hint]}" when class="s">"dependency cycle" puts class="s">" Dependency cycle - the plan can never start:" puts class="s">" > #{f[class="y">:snippet].join(" -> class="s">")}" puts class="s">" every member of the loop waits for another member. one of" puts class="s">" these edges is aspirational, not structural - probably the" puts class="s">" newest one." end puts end end puts class="s">"THE ASSEMBLY DOCTOR (error messages are the UI of failure)" puts findings = diagnose(PLAN) puts class="s">" examining the 5pm deploy plan (#{PLAN.size} steps): #{findings.size} wounds found" puts print_diagnosis(PLAN, findings) # --- the repaired plan must actually run - doctors get audited too ----------------- fixed = PLAN.map { |s| {step: s[class="y">:step], after: Array(s[class="y">:after]).map { |d| (d == class="s">"fetch metdata") ? class="s">"fetch metadata" : d } - [class="s">"verify health"]} } fixed[5][class="y">:after] = [class="s">"install deps"] # run migrations no longer waits on verify health abort(class="s">" doctor still sees wounds in the fixed plan!") unless diagnose(fixed).empty? orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 4) tasks = {} fixed.each do |s| tasks[s[class="y">:step]] = Agentic:class="y">:Task.new(description: s[class="y">:step], agent_spec: {class="s">"name" => s[class="y">:step], class="s">"instructions" => class="s">"go"}) orchestrator.add_task(tasks[s[class="y">:step]], s[class="y">:after].map { |d| tasks.fetch(d) }, agent: ->(_t) { class="s">"ok" }) end result = orchestrator.execute_plan puts class="s">" after the two one-line fixes: doctor finds nothing; plan runs: #{result.status}" puts puts class="s">" the doctor's rules are syntax_suggest's rules, one abstraction up:" puts class="s">" don't report the symptom's location, report the SMALLEST REGION" puts class="s">" that explains it (the typo'd edge, the whole loop); show the code," puts class="s">" not a stack trace; and when the system is holding the list of" puts class="s">" valid names - it always is - spend one Levenshtein pass to turn" puts class="s">" the diagnosis into a one-keystroke fix. the framework's own" puts class="s">" Suggestions module supplied the did-you-mean. people don't quit" puts class="s">" because programming is hard; they quit because failure is rude." exit((findings.size == 2 && result.status == class="y">:completed) ? 0 : 1)