It was 3:03 AM when I stumbled upon GitHub Issue #39.
Nine years old. Still open. Five upvotes. The kind of feature request that haunts every maintainer’s dreams—simple enough to implement in an afternoon, complex enough to break everything you’ve ever loved.
“Passing event arguments to guards”
I stared at the screen, my energy drink suddenly became warm. @rosskevin had opened this in 2016, back when Ruby 2.3 was considered “modern” and we still believed JavaScript frameworks would eventually stabilize. Now here I was in 2025, Captain Seuros of the good ship state_machines
, about to do what nine years of maintainer wisdom had said was too dangerous.
Time to arm the guards.
The Code Prisoners
First, let me introduce my fellow inmate: @rosskevin. We’re both prisoners in the state_machines
organization—not by choice, but by the cruel fate that befalls anyone foolish enough to fix a bug in an abandoned Ruby gem. One pull request, and suddenly you’re a “maintainer.” One accepted patch, and you’re serving a life sentence in GitHub notification hell.
We’re like the guys who got promoted to captain by being the only ones left alive on the ship.
But here’s the thing about being co-maintainers: we protect each other. When one of us gets the brilliant idea to implement a “simple” feature request, the other one talks them down from the ledge. Because we’ve seen what happens when you promise the community something that works perfectly on CRuby but crashes spectacularly on JRuby.
“Remember,” we tell each other, “legacy support isn’t just technical debt—it’s a loaded gun pointed at your motivation to contribute.”
The 9-Year Itch
Let me take you back to 2016. @rosskevin had a subscription model that needed conditional transitions:
# The dream API from 2016
event :start do
transition :uninitialized => :trial, if: ->(subscription, args) {
subscription.trial_enabled? && !args.include?(:skip_trial)
}
transition [:uninitialized, :trial] => :active
end
subscription.start(:skip_trial) # Skip the trial, go straight to active
Simple, right? Just pass the event arguments to the guard condition. What could go wrong?
Everything.
The moment you implement this feature, you’re promising the Ruby community that it works everywhere Ruby works. CRuby? Check. JRuby? Better work. TruffleRuby? Don’t even think about shipping without testing it. And if some weird edge case breaks on Ruby 3.7 in 2028, guess who gets blamed?
That’s why we sat on this issue for nine years. Not because we couldn’t implement it—because we were terrified of what would happen when we did.
The Fear of Implementation
Here’s what normal people don’t understand about maintainer psychology: every feature you add is a promise you have to keep forever. And “forever” in open source means “until you burn out, give up, and some poor soul inherits your technical debt.”
You know what happens when you implement a clever feature that only works on one Ruby implementation? You get midnight GitHub notifications from angry developers whose builds are failing because they dared to use JRuby. You get passive-aggressive pull requests that “fix” your implementation by removing the part that makes it useful.
You get people asking why your gem doesn’t “just work” like every other gem, as if backward compatibility across three Ruby implementations and fifteen years of legacy code is something you can solve with a Stack Overflow answer.
So we did what every rational maintainer does: we put the issue in the “someday” pile and quietly implemented hacky workarounds in our own applications.
By 2023, the community was getting restless. @joegaudet dropped the comment that haunts every maintainer’s dreams:
“I take this is dead in the water as of now, would love to have this capability.”
Dead in the water? Bro, we’re in outer space. The Open Source Ship isn’t going to build itself. Sometimes you have to stop orbiting the problem and actually land on the solution.
Then @maedi offered a thoughtful workaround that actually helped people ship their code:
def update(event, condition, argument)
@condition = condition
@argument = argument
fire_state_event(event)
end
def stoppable?
@condition
end
This is the kind of community contribution that keeps projects alive—practical solutions that let developers get their work done while maintainers figure out the “proper” implementation. But it also reminded me why we needed the real feature: in space, when you have a hull breach, you don’t reach for the Flex Tape. You fix the structural problem. These workarounds are brilliant emergency patches, but they’re still patches—using instance variables to smuggle state between methods because the API doesn’t support what you actually need.
It worked. It was ugly. It kept our sanity intact. But it also meant that every developer using our gem had to invent their own version of this hack. And that’s not just bad API design—it’s maintainer cowardice disguised as prudence.
The Moroccan Solution
Fast forward to June 2025. I’d just finished the surgical refactoring that broke a 1,647-line monolith into focused modules, dropping legacy Ruby support, and generally burning bridges with the past. As I was about to close my laptop at 3 AM, one final GitHub notification caught my eye.
“You know what?” I thought. “If I’m already breaking backward compatibility, why not fix this 9-year-old issue too? Maybe I’ll tackle all the ancient issues and finally implement my retirement strategy: auto-charge $1000 for every new GitHub issue. RTFM violations? We keep the cash. Legitimate bugs? The reporter gets a shiny badge and we fix it.”
Time for some Moroccan naval engineering.
Meet the RMNS Atlas Monkey (Royal Moroccan Naval Ship), my test vessel for this feature. Like other nations buying Starfleet vessels from the Federation, then adding their own small modifications—that’s inheritance. But if you don’t rebrand the ship? That’s just monkey patching a USS into an RMNS.
# RMNS Atlas Monkey - Moroccan engineering at its finest! 🇲🇦🐒🚀
class RmnsAtlasMonkey < StarfleetShip
state_machine :status do
event :engage_warp do
# Emergency override: bypass safety protocols when needed
transition impulse: :warp, if: ->(ship, *args) {
ship.send(:warp_core_stable?) || args.include?(:emergency_override)
}
end
end
end
The Starfleet ships follow regulations. The RMNS Atlas Monkey follows results.
# Starfleet protocol: safety first
starfleet_ship.warp_core_temperature = 2000 # Unstable
starfleet_ship.engage_warp # => false (safety protocols engaged)
# Moroccan protocol: mission first
moroccan_ship.warp_core_temperature = 2000 # Unstable
moroccan_ship.engage_warp(:emergency_override) # => true (mission complete)
Because sometimes you need to get to the Neutral Zone, regulations be damned.
The Technical Implementation
Here’s where it gets interesting. The challenge wasn’t implementing the feature—it was doing it without breaking every existing guard condition in the wild.
The solution? Arity detection.
def evaluate_method_with_event_args(object, method, event_args = [])
case method
when Proc
arity = method.arity
if arity == 1
# Backward compatible: only pass the object
method.call(object)
elsif arity == -1 || arity > 1
# New behavior: pass object + event arguments
method.call(object, *event_args)
end
end
end
Existing guards with single parameters keep working exactly as before:
# This still works perfectly
event :engage_warp do
transition impulse: :warp, if: :warp_core_stable?
end
# So does this
event :engage_warp do
transition impulse: :warp, if: ->(ship) { ship.warp_core_stable? }
end
But now you can write guards that receive event arguments:
# The new hotness
event :fire_at_target do
transition targeted: :firing, if: ->(ship, target_type, *args) {
case target_type
when :asteroid
true # Can always fire at asteroids
when :enemy_ship
ship.shields_up? # Need shields for combat
when :photon_torpedo
args.include?(:full_spread) # Special firing pattern
else
false
end
}
end
# Usage
ship.fire_at_target_weapons(:photon_torpedo, :full_spread) # Works!
ship.fire_at_target_weapons(:photon_torpedo) # Fails safely
The beauty of arity detection is that it’s completely transparent. Existing code doesn’t know the feature exists. New code can opt into it by changing their lambda signature.
The Testing Drama
Of course, you can’t just implement a feature and call it done. You need tests that prove it works across all the scenarios that kept you awake at night for nine years.
Enter the RMNS Atlas Monkey integration tests:
def test_event_arguments_allow_emergency_override_in_guards
# Normal operation: should work when warp core is stable
@ship.undock
@ship.warp_core_temperature = 1500 # Stable
assert_sm_can_transition(@ship, :engage_warp, machine_name: :status)
assert @ship.engage_warp
assert_sm_state(@ship, :warp, machine_name: :status)
# Reset ship
@ship.drop_to_impulse
assert_sm_state(@ship, :impulse, machine_name: :status)
# Unstable core: should fail without override
@ship.warp_core_temperature = 2000 # Unstable
assert_sm_cannot_transition(@ship, :engage_warp, machine_name: :status)
refute @ship.engage_warp
assert_sm_state(@ship, :impulse, machine_name: :status)
# Unstable core with emergency override: should succeed
assert @ship.engage_warp(:emergency_override)
assert_sm_state(@ship, :warp, machine_name: :status)
end
This isn’t just testing the feature—it’s testing the psychology of the feature. Does it feel natural? Does it do what developers expect? Can you explain it to someone without a computer science degree?
The weapons targeting test was even better:
def test_event_arguments_support_conditional_logic_in_guards
# Setup: arm and target weapons
@ship.arm_weapons
@ship.target_weapons
# Test firing at asteroid (always allowed)
assert @ship.fire_at_target_weapons(:asteroid)
assert_sm_state(@ship, :firing, machine_name: :weapons)
# Reset weapons
@ship.reload_weapons
@ship.target_weapons
# Test firing at enemy ship without shields (should fail)
refute @ship.fire_at_target_weapons(:enemy_ship)
assert_sm_state(@ship, :targeted, machine_name: :weapons)
# Test firing at enemy ship with shields (should succeed)
@ship.raise_shields
assert @ship.fire_at_target_weapons(:enemy_ship)
assert_sm_state(@ship, :firing, machine_name: :weapons)
# Test photon torpedo with full spread (should succeed)
@ship.reload_weapons
@ship.target_weapons
assert @ship.fire_at_target_weapons(:photon_torpedo, :full_spread)
assert_sm_state(@ship, :firing, machine_name: :weapons)
end
The RMNS Atlas Monkey proved that Moroccan engineering could handle complex targeting protocols that rigid Starfleet procedures would never allow. This ship runs on the Inchallah Core module—where every guard condition is a leap of faith that either delivers victory or sends you straight into a black hole.
But here’s the thing about Inchallah engineering: it comes with test and verification, else it’s totally haram. Notice those assert_sm_state
and assert_sm_can_transition
helpers? Those came from the state_machines test helper that I also improved during this implementation. Because if you’re going to arm the guards, you might as well give developers better weapons to test with.
The Backward Compatibility Dance
The real test wasn’t whether the new feature worked—it was whether the old features still worked. Because the moment you break existing code, you’ve gone from “helpful maintainer” to “that guy who ruined my deployment.”
def test_backward_compatibility_with_existing_guards
# Use the original StarfleetShip to verify existing guards still work
original_ship = StarfleetShip.new
original_ship.undock
original_ship.warp_core_temperature = 1500 # Stable
# The existing warp_core_stable? guard should still work
assert original_ship.engage_warp
assert_sm_state(original_ship, :warp, machine_name: :status)
end
This test exists for one reason: to prove that I didn’t break anyone’s existing code while implementing the new feature. It’s the maintainer’s insurance policy against angry GitHub issues titled “Your latest release broke my entire application.”
But the real confidence came from the comprehensive unit tests. I wrote 23 different test scenarios covering every edge case I could imagine:
def test_proc_arity_detection
# Test various arity scenarios
# Arity 0: -> { true }
# Arity 1: ->(obj) { obj.valid? }
# Arity 2: ->(obj, arg) { obj.valid? && arg == :test }
# Arity -1 (splat): ->(obj, *args) { obj.valid? && args.include?(:test) }
end
def test_complex_use_case_from_github_issue
# Test the exact use case from GitHub issue #39
@machine.event :start do
transition :uninitialized => :trial, if: ->(subscription, *args) {
subscription.trial_enabled? && (args.empty? || args[0] != true)
}
transition [:uninitialized, :trial] => :active
end
end
Every permutation of guard types, every arity combination, every backward compatibility scenario. If someone could break this feature, I wanted to find out in the test suite, not in production.
The Mixed Guard Types Challenge
But wait, there’s more complexity. What happens when you have multiple guard types in the same event?
event :emergency_warp do
# Multi-param lambda guard (needs proper authorization)
transition impulse: :warp, if: ->(ship, *args) {
args.length >= 2 && args[0] == "omega-3-7" && args[1] == :confirmed
}
# Symbol guard (existing behavior)
transition impulse: :warp, if: :warp_core_stable?
# Single-param lambda guard (existing behavior)
transition impulse: :warp, if: ->(ship) { ship.captain_on_bridge }
end
The state machine evaluates guards in order, trying each transition until one succeeds. This means:
# Wrong authorization code - first guard fails, tries second
ship.emergency_warp("wrong-code") # Uses warp_core_stable? instead
# Correct authorization - first guard succeeds immediately
ship.emergency_warp("omega-3-7", :confirmed) # Bypasses all other checks
It’s like having multiple security clearance levels. The RMNS Atlas Monkey can use emergency authorization codes that bypass normal safety protocols, but it falls back to standard procedures if the codes are wrong.
The Cross-Platform Panic
Now comes the moment every maintainer dreads: testing across Ruby implementations.
CRuby: ✅ Works perfectly JRuby: ✅ Works perfectly TruffleRuby: ✅ Works perfectly
Wait, that can’t be right. Let me test again.
CRuby: ✅ Still works JRuby: ✅ Still works TruffleRuby: ✅ Still works
Huh. Turns out that when you implement a feature using basic language primitives like arity
and call
, it just works everywhere Ruby works. Who knew?
The reason this feature sat unimplemented for nine years wasn’t technical complexity—it was maintainer paranoia. We were so afraid of breaking something that we never tried the simple solution.
The API Evolution
The beautiful thing about this implementation is how it evolves your thinking about state machine design. Before, guards were just yes/no questions:
# Old thinking: guards are binary checks
if: :user_authorized?
if: :subscription_active?
if: :warp_core_stable?
Now, guards can be contextual decisions:
# New thinking: guards are contextual evaluators
if: ->(user, action_type) { user.can_perform?(action_type) }
if: ->(subscription, plan_type) { subscription.can_upgrade_to?(plan_type) }
if: ->(ship, override_code) { ship.warp_core_stable? || override_code == :emergency }
It’s the difference between a lock that only recognizes one key, and a lock that can evaluate different types of credentials.
The Community Response
Remember, this was an 8-year-old issue. The moment I pushed the implementation, developers came out of the woodwork with use cases I’d never considered:
“Finally! I can implement approval workflows with rejection reasons!”
“This is perfect for payment processing with different risk levels!”
“I’ve been monkey patching this behavior for years!”
The feature that maintainers were afraid to implement for fear of complexity turned out to solve problems we didn’t even know existed.
Lessons from the Trenches
1. Maintainer paralysis is real. Sometimes the fear of implementing a feature is worse than the actual implementation.
2. Backward compatibility doesn’t require stagnation. You can add new behavior without breaking existing behavior, if you’re clever about it.
3. The simple solution is often the right solution. Arity detection isn’t fancy, but it works reliably across all Ruby implementations.
4. Cross-platform testing reveals fewer problems than you expect. Most Ruby features work the same way everywhere.
5. Community demand is a powerful motivator. Eight years of “is this feature coming?” creates pressure to finally ship something.
The Moroccan Naval Advantage
The RMNS Atlas Monkey proved something important: sometimes you need to break from rigid protocols to get things done. Starfleet ships follow regulations. Moroccan naval vessels follow results.
# Starfleet approach: everything by the book
event :engage_warp do
transition impulse: :warp, if: :all_systems_nominal?
transition impulse: :warp, if: :warp_core_stable?
transition impulse: :warp, if: :captain_authorization?
end
# RMNS approach: mission-focused flexibility
event :engage_warp do
transition impulse: :warp, if: ->(ship, *args) {
ship.all_systems_nominal? ||
(ship.warp_core_stable? && ship.captain_authorized?) ||
args.include?(:emergency_override)
}
end
The difference? The Moroccan ship gets to the destination. The Starfleet ship follows procedure.
The Future of State Guards
This feature opens up possibilities that go way beyond the original 2016 request:
Conditional Workflows:
event :submit_for_approval do
transition draft: :pending, if: ->(doc, approval_type) {
case approval_type
when :fast_track then doc.author.senior_level?
when :standard then doc.complete?
when :emergency then doc.author.director_level?
else false
end
}
end
Dynamic Risk Assessment:
event :process_payment do
transition pending: :approved, if: ->(payment, risk_level) {
case risk_level
when :low then payment.amount < 100
when :medium then payment.amount < 1000 && payment.user.verified?
when :high then payment.manual_review_passed?
else false
end
}
end
Context-Aware Authorization:
event :access_resource do
transition locked: :open, if: ->(resource, user, context) {
user.can_access?(resource) &&
context[:ip_address].in?(resource.allowed_networks)
}
end
The state machine becomes less of a rigid flowchart and more of an intelligent decision engine.
The Victory Lap
At 4:23 AM, I pushed the final commit to the arming-guards
branch. Eight years of maintainer fear, resolved in three days of actual development.
The tests passed. The implementation was clean. The backward compatibility was perfect. The RMNS Atlas Monkey had proven that Moroccan engineering could solve problems that Starfleet bureaucracy couldn’t touch.
But the real victory wasn’t technical—it was psychological. We’d broken free from the maintainer paralysis that keeps useful features trapped in “someday” piles.
Sometimes the courage to ship is more valuable than the feature itself.
The GitHub Resolution
Issue #39 is finally closed. Not with “won’t fix” or “this is too complex” or “use a workaround”—but with actual, working code that does exactly what the community asked for nine years ago.
The final comment on the issue was simple:
Implemented in v0.30.0. Guards now receive event arguments based on arity. Backward compatible. Thanks for your patience!
— Captain Seuros, RMNS Atlas Monkey 🇲🇦🐒🚀
Because sometimes, the best response to a 9-year-old feature request is a ship that actually delivers.
Next time: “The Macro Methods Massacre” - Part 3 of the state_machines refactoring series, where we dismantle the 98.5% documentation monster that terrorized our codebase.