You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Both Run and Clean phases use the same state names:
State
Description
pending
Initial state, waiting to be executed
running
Task is currently executing
completed
Task finished successfully
failed
Task finished with error
skipped
Task was not executed (Run phase only)
skipped Reasons
The skipped state means "this task was not executed." The reason can be determined by observers via Pull (checking dependency graph and dependency states):
Not accessed at runtime: Static analysis detected the task as a possible dependency, but the runtime code path never accessed it (e.g., if branch not taken, rescue block not entered). This is a natural result of Fiber-based lazy resolution — only actually accessed dependencies are executed.
Dependency failed: A task this depends on failed, so this task could not run
This keeps skipped as a simple factual statement ("not executed") rather than encoding the reason in the state.
Removed States
enqueued / clean_enqueued: Removed. The current Scheduler uses enqueued as an intermediate state, but running is sufficient. Once a task begins execution, it is considered running.
Clean State Unification
The current clean states (:cleaning, :clean_completed, :clean_failed) are unified to use the same names as run states (:running, :completed, :failed). The phase context (:run or :clean) distinguishes which phase the state belongs to.
Clean State Special Case
nil: Tasks that were skipped or failed in Run phase don't have a Clean phase
Push/Pull Pattern
Push (Notification)
Notify identifiers, state transition, phase, and timestamp:
All transient/mutable information is pushed. The facade itself holds zero mutable domain state.
Pull (Query)
Any component that needs execution-related information pulls immutable data from ExecutionFacade:
facade.root_task_class# Root task (immutable)facade.dependency_graph# DependencyGraph (immutable, single source for all dependency/tree data)facade.output_stream.read(task_class,limit: nil)# Captured output (accumulating)
ExecutionFacade is the single pull target — observers, Executor, and any other component that needs execution data should pull from here.
Design Principle: Stateless Facade
The facade holds no mutable domain state. The split is:
Type
Where
Examples
Mutable/transient
Push (in notifications)
phase, previous_state, current_state, timestamp
Immutable
Pull (from facade)
dependency_graph, root_task_class
Accumulating
Pull (from facade)
output_stream
Key Decisions
previous_state in notifications: Allows observers to determine state transitions directly
pending→running: Task started
pending→skipped: Task not executed (not accessed at runtime, or dependency failed)
running→completed: Success
running→failed: Error
phase in notifications: Included in every task_updated so observers always know which phase a state transition belongs to, without needing to track phase state themselves. Eliminates facade.current_phase as mutable state.
state_for is unnecessary: State is pushed via notification
No error in notifications: Error details (message, backtrace) are logged via a direct path from Executor → Taski::Logging, not through the observer notification system. Ruby's standard exception propagation handles user-facing error display at the top level. Including error in the notification would create inconsistent signatures — pending→running and pending→skipped transitions have no error, only running→failed does.
No duration in notifications: Duration is not a fundamental piece of data — it's derived from timestamps. Observers calculate duration themselves:
Record timestamp on pending→running transition
Compute duration = timestamp - start_time on running→completed/failed transition
This keeps the notification signature clean and uniform across all transitions. TaskTiming in TaskWrapper (which existed solely to calculate and pass duration_ms to observers) is removed.
All task candidates known before execution: Static analysis detects all possible task candidates including their dependency trees. The only thing determined at runtime is which tasks are NOT executed (skipped) — this is determined by Fiber-based lazy resolution (only accessed dependencies execute).
Single DependencyGraph: The dependency graph and tree display metadata are derived from the same static analysis data. Instead of maintaining two separate structures (dependency_graph + task_tree), a single DependencyGraph serves as the sole source for all dependency and tree-related queries. Layout observers derive display metadata (depth, position, tree prefixes) from the graph — the graph itself does not carry display concerns.
Event Catalog (8 Events)
Category
Events
Count
Lifecycle
ready, start, stop
3
Phase
phase_started(phase), phase_completed(phase)
2
Task
task_updated
1
Group
group_started, group_completed
2
Event Signatures
# Lifecycle (3)notify_ready()# Preparation complete (Pull for root_task, graph, all tasks)notify_start()notify_stop()# Phase (2)notify_phase_started(phase)# phase = :run or :cleannotify_phase_completed(phase)# Task (1)notify_task_updated(task_class,previous_state:,current_state:,phase:,timestamp:)# Group (2)notify_group_started(task_class,group_name,phase:,timestamp:)notify_group_completed(task_class,group_name,phase:,timestamp:)
classMyLayout < TaskObserverdefon_ready@graph=@context.dependency_graph# Single source for all dependency/tree data@root=@context.root_task_class@start_times={}enddefon_task_updated(task_class,previous_state:,current_state:,phase:,timestamp:)case[previous_state,current_state]when[:pending,:running]@start_times[task_class]=timestampwhen[:pending,:skipped]mark_skipped(task_class)when[:running,:completed],[:running,:failed]duration=timestamp - @start_times[task_class]render_completion(task_class,current_state,duration)endendend# ExecutionFacade auto-injects at registrationfacade.add_observer(my_layout)# → my_layout.context = self
ExecutionFacade
Architectural Intent
The core motivation of this refactoring is to consolidate scattered execution-related information into a single object. Currently, the dependency tree is built independently in multiple places (Scheduler, Layout::Base, etc.), and runtime information is spread across ExecutionContext, Scheduler, TaskWrapper, and Layout.
ExecutionFacade absorbs the current ExecutionContext and becomes the single source of truth for all execution-related information. It is created once per execution and referenced by any component that needs execution data.
Key insight: The dependency graph is immutable — it is built once from static analysis results and never changes during execution. What changes is each task's state, which is pushed via notifications. The facade itself holds no mutable domain state — all transient information (phase, state transitions, timestamps) flows through notifications.
Single DependencyGraph
Currently, dependency data is computed independently in multiple places:
DependencyGraph — builds graph via Analyzer.analyze() recursively
Layout::Base#build_tree_node — re-runs Analyzer.analyze() to build a tree
Scheduler#build_dependency_graph — builds its own graph via BFS
These all derive from the same static analysis data. In the new design, DependencyGraph is the single data structure built once by ExecutionFacade. It provides:
Tree traversal: parent-child relationships derived from the graph structure
Layout observers derive their own display metadata (depth, is_last flags, tree prefixes) from the graph in on_ready. The graph itself does not carry display concerns.
Initialization: Static Analysis → ExecutionFacade
ExecutionFacade receives static analysis results and builds the dependency graph at initialization:
# Static analysis is performed onceanalysis_result=StaticAnalysis::Analyzer.analyze(root_task_class)# ExecutionFacade builds the dependency graph from the analysis resultfacade=ExecutionFacade.new(root_task_class: root_task_class,analysis_result: analysis_result,output_hub: output_hub)# Internally builds:# - dependency_graph (single source: circular detection, execution order, tree traversal)
The graph is built once and frozen. Consumers pull what they need:
Layout pulls dependency_graph and derives display metadata (depth, tree prefixes) in on_ready
Any component pulls what it needs without rebuilding
Before: Information scattered, static analysis re-run in multiple places
Scheduler → builds dependency graph (BFS from cached_dependencies)
Layout::Base → re-runs static analysis to build tree
TaskState class tracks per-task state (run_state, clean_state, duration, error)
ExecutionContext → observer dispatch + output capture
TaskWrapper → state, timing, error
After: Static analysis → ExecutionFacade builds DependencyGraph once
StaticAnalysis::Analyzer.analyze(root)
│
▼
ExecutionFacade (single source of truth, created once per execution)
├── dependency_graph (immutable — single source for graph & tree queries)
├── root_task_class (immutable)
├── output_stream (OutputHub, accumulating)
├── observer notification dispatch (stateless — passes data through)
└── NO mutable domain state
Layout derives display metadata from dependency_graph in on_ready:
└── depth, is_last, tree prefixes (computed once, stored in Layout)
Referenced by:
├── Executor → triggers notifications (phase, state transitions)
├── Layout → receives notifications + pulls dependency_graph, output_stream
└── any component that needs execution data
Note: This intentionally creates a fat model. The goal is to first gather all scattered information into one place, understand the full picture, and then decompose into well-separated components in a future refactoring.
API
classExecutionFacadedefinitialize(root_task_class:,analysis_result:,output_hub:)@root_task_class=root_task_class@output_hub=output_hub# Build single immutable structure from static analysis@dependency_graph=build_dependency_graph(analysis_result)@observers=[]end# --- Pull API (immutable data only) ---attr_reader:root_task_class# Immutableattr_reader:dependency_graph# Immutable — single source for all dependency/tree datadefoutput_stream# Accumulating@output_hubend# --- Push API (stateless dispatch — no domain state stored) ---defadd_observer(observer)observer.context=self@observers << observerenddefnotify_task_updated(task_class,previous_state:,current_state:,phase:,timestamp:)dispatch(:on_task_updated,task_class,previous_state:,current_state:,phase:,timestamp:)end# ... other notify_* methodsprivatedefdispatch(method, *args, **kwargs)@observers.eachdo |observer|
observer.send(method, *args, **kwargs)ifobserver.respond_to?(method)endendend
Output Handling
Design
Both stdout and stderr are captured (per-fiber scoping already implemented by #155):
Source
Destination
Captured
Task stdout (puts)
OutputHub
Yes
Task stderr (warn)
OutputHub
Yes
Layout/Progress display
original_stderr
No
OutputHub Implementation
classOutputHubMAX_RECENT_LINES=30defstore_output_lines(task_class,data)lines=data.lines.map(&:chomp)lines.eachdo |line|
# Immediate log outputTaski::Logging.output(task_class,line)# Store in ring buffer@ring_buffer[task_class] ||= []@ring_buffer[task_class] << line@ring_buffer[task_class]=@ring_buffer[task_class].last(MAX_RECENT_LINES)endend# Public API: read# limit: nil for entire buffer, or last N linesdefread(task_class,limit: nil)lines=@ring_buffer[task_class] || []limit ? lines.last(limit) : linesendend
Error Logging
Error details (message, backtrace) are logged via a direct path, not through the observer notification system:
# In Executor: when a task failsrescue=>errorTaski::Logging.error(task_class,error)# Direct path: log error details immediatelywrapper.mark_failed(error)# Triggers notify_task_updated(state: :failed)end
Includes all possible dependencies from all code paths
Runtime
Actual execution
Fiber-based lazy resolution
Only actually accessed dependencies are executed
The gap between static and runtime dependencies is the set of skipped tasks. Static analysis detects all possible tasks; at runtime, Fiber lazy resolution only executes the ones actually accessed. The rest are marked skipped.
Removed Classes / Code
TaskTiming — Remove
The TaskTiming class in TaskWrapper (tracks start_time, end_time, calculates duration_ms) is removed. With the new design:
Timing: Each task_updated notification includes timestamp:. Observers record the timestamp at pending→running and compute duration at running→completed/failed by calculating the difference.
TaskTiming existed solely to calculate and pass duration_ms to observers. With timestamp-based notifications, this is no longer needed.
Remove @timing and @clean_timing instance variables from TaskWrapper.
Layout::Base::TaskState — Remove
The current TaskState class in Layout::Base (tracks run_state, clean_state, run_duration, clean_duration, run_error, clean_error per task) is removed. With the new design:
State: Pushed via task_updated notifications — each observer tracks what it needs
Duration: Calculated by observers from timestamp in notifications
Error: Logged via direct path (Executor → Taski::Logging), not stored per-task
Observers are responsible for maintaining their own view of task state based on notifications they receive.
Tree Building Code in Layout — Remove
All tree building and dependency analysis code must be removed from Layout classes. This logic is replaced by a single DependencyGraph built once by ExecutionFacade. Layout observers derive display metadata from the graph in on_ready.
Layout::Base — remove the following methods:
build_tree_node — recursive tree node construction (replaced by dependency_graph)
on_root_task_set — no longer needed (replaced by on_ready pulling from Facade)
set_root_task — no longer needed (root_task available via Facade Pull)
@tasks hash (TaskState tracking) — observers track their own state
apply_state_transition — replaced by unified task_updated notification handling
Layout::Simple — remove:
build_tree_structure — orchestrates tree building
register_tasks_from_tree — registers tasks from tree into @tasks
on_root_task_set override
Layout::Tree — remove:
build_tree_structure — orchestrates tree building
register_tree_nodes — registers tree nodes with depth/position metadata
on_root_task_set override
Rebuild display metadata from facade.dependency_graph in on_ready
Scheduler#build_dependency_graph — Remove
Scheduler currently builds its own dependency graph via BFS from cached_dependencies. This is replaced by pulling dependency_graph from ExecutionFacade.
Scheduler state constants — Remove
STATE_ENQUEUED / CLEAN_STATE_ENQUEUED: No longer needed (unified into running)
ExecutionContext — Remove (absorbed into ExecutionFacade)
All functionality (observer dispatch, output capture management) moves to ExecutionFacade. Individual notify_* methods are replaced by the unified event catalog.
Current methods to remove/merge:
notify_task_registered → removed (Pull at ready)
notify_task_started / notify_task_completed / notify_task_skipped → merged into notify_task_updated
notify_clean_started / notify_clean_completed → merged into notify_task_updated
notify_set_root_task / notify_set_output_capture → merged into notify_ready
notify_start / notify_stop → kept as notify_start / notify_stop
Summary
Define Taski's event and state model clearly to provide a consistent observer notification system for progress displays, logging, and user hooks.
Prerequisites: This issue assumes the following are already completed:
Task.clean/Task.newremoved,run_and_cleanblock support addedState Diagrams
Run Phase
stateDiagram-v2 [*] --> pending pending --> running : task starts pending --> skipped : not executed running --> completed : success running --> failed : error completed --> [*] failed --> [*] skipped --> [*]Clean Phase
stateDiagram-v2 [*] --> pending pending --> running : clean starts running --> completed : success running --> failed : error completed --> [*] failed --> [*]Unified State Set
Both Run and Clean phases use the same state names:
pendingrunningcompletedfailedskippedskippedReasonsThe
skippedstate means "this task was not executed." The reason can be determined by observers via Pull (checking dependency graph and dependency states):ifbranch not taken,rescueblock not entered). This is a natural result of Fiber-based lazy resolution — only actually accessed dependencies are executed.This keeps
skippedas a simple factual statement ("not executed") rather than encoding the reason in the state.Removed States
enqueued/clean_enqueued: Removed. The current Scheduler usesenqueuedas an intermediate state, butrunningis sufficient. Once a task begins execution, it is consideredrunning.Clean State Unification
The current clean states (
:cleaning,:clean_completed,:clean_failed) are unified to use the same names as run states (:running,:completed,:failed). The phase context (:runor:clean) distinguishes which phase the state belongs to.Clean State Special Case
nil: Tasks that wereskippedorfailedin Run phase don't have a Clean phasePush/Pull Pattern
Push (Notification)
Notify identifiers, state transition, phase, and timestamp:
All transient/mutable information is pushed. The facade itself holds zero mutable domain state.
Pull (Query)
Any component that needs execution-related information pulls immutable data from ExecutionFacade:
ExecutionFacade is the single pull target — observers, Executor, and any other component that needs execution data should pull from here.
Design Principle: Stateless Facade
The facade holds no mutable domain state. The split is:
phase,previous_state,current_state,timestampdependency_graph,root_task_classoutput_streamKey Decisions
previous_state in notifications: Allows observers to determine state transitions directly
pending→running: Task startedpending→skipped: Task not executed (not accessed at runtime, or dependency failed)running→completed: Successrunning→failed: Errorphase in notifications: Included in every
task_updatedso observers always know which phase a state transition belongs to, without needing to track phase state themselves. Eliminatesfacade.current_phaseas mutable state.state_for is unnecessary: State is pushed via notification
No error in notifications: Error details (message, backtrace) are logged via a direct path from Executor →
Taski::Logging, not through the observer notification system. Ruby's standard exception propagation handles user-facing error display at the top level. Including error in the notification would create inconsistent signatures —pending→runningandpending→skippedtransitions have no error, onlyrunning→faileddoes.No duration in notifications: Duration is not a fundamental piece of data — it's derived from timestamps. Observers calculate duration themselves:
timestamponpending→runningtransitionduration = timestamp - start_timeonrunning→completed/failedtransitionThis keeps the notification signature clean and uniform across all transitions.
TaskTimingin TaskWrapper (which existed solely to calculate and passduration_msto observers) is removed.All task candidates known before execution: Static analysis detects all possible task candidates including their dependency trees. The only thing determined at runtime is which tasks are NOT executed (skipped) — this is determined by Fiber-based lazy resolution (only accessed dependencies execute).
Single DependencyGraph: The dependency graph and tree display metadata are derived from the same static analysis data. Instead of maintaining two separate structures (
dependency_graph+task_tree), a singleDependencyGraphserves as the sole source for all dependency and tree-related queries. Layout observers derive display metadata (depth, position, tree prefixes) from the graph — the graph itself does not carry display concerns.Event Catalog (8 Events)
ready,start,stopphase_started(phase),phase_completed(phase)task_updatedgroup_started,group_completedEvent Signatures
Execution Order
Removed Events (vs current ExecutionContext)
notify_task_registered→ Removed (all task candidates known before execution via Pull atready)notify_task_started/notify_task_completed/notify_task_skipped→ Merged intonotify_task_updatednotify_clean_started/notify_clean_completed→ Merged intonotify_task_updatedwith phase contextnotify_set_root_task/notify_set_output_capture→ Merged intonotify_readyObserver Design
Base Class
Usage Example
ExecutionFacade
Architectural Intent
The core motivation of this refactoring is to consolidate scattered execution-related information into a single object. Currently, the dependency tree is built independently in multiple places (Scheduler, Layout::Base, etc.), and runtime information is spread across ExecutionContext, Scheduler, TaskWrapper, and Layout.
ExecutionFacade absorbs the current ExecutionContext and becomes the single source of truth for all execution-related information. It is created once per execution and referenced by any component that needs execution data.
Key insight: The dependency graph is immutable — it is built once from static analysis results and never changes during execution. What changes is each task's state, which is pushed via notifications. The facade itself holds no mutable domain state — all transient information (phase, state transitions, timestamps) flows through notifications.
Single DependencyGraph
Currently, dependency data is computed independently in multiple places:
DependencyGraph— builds graph viaAnalyzer.analyze()recursivelyLayout::Base#build_tree_node— re-runsAnalyzer.analyze()to build a treeScheduler#build_dependency_graph— builds its own graph via BFSThese all derive from the same static analysis data. In the new design,
DependencyGraphis the single data structure built once by ExecutionFacade. It provides:all_tasks,dependencies_for(task_class),sorted(topological order),cyclic?Layout observers derive their own display metadata (depth,
is_lastflags, tree prefixes) from the graph inon_ready. The graph itself does not carry display concerns.Initialization: Static Analysis → ExecutionFacade
ExecutionFacade receives static analysis results and builds the dependency graph at initialization:
The graph is built once and frozen. Consumers pull what they need:
dependency_graphand derives display metadata (depth, tree prefixes) inon_readyAPI
Output Handling
Design
Both stdout and stderr are captured (per-fiber scoping already implemented by #155):
puts)warn)OutputHub Implementation
Error Logging
Error details (message, backtrace) are logged via a direct path, not through the observer notification system:
task_updated)task_class,previous_state,current_state,phase,timestampTaski::LoggingTaski::LoggingResponsibility
@ring_buffer(per task)read(task_class, limit:)Two Types of Dependencies
StaticAnalysis::Analyzer.analyze()The gap between static and runtime dependencies is the set of
skippedtasks. Static analysis detects all possible tasks; at runtime, Fiber lazy resolution only executes the ones actually accessed. The rest are markedskipped.Removed Classes / Code
TaskTiming— RemoveThe
TaskTimingclass inTaskWrapper(tracksstart_time,end_time, calculatesduration_ms) is removed. With the new design:task_updatednotification includestimestamp:. Observers record the timestamp atpending→runningand compute duration atrunning→completed/failedby calculating the difference.TaskTimingexisted solely to calculate and passduration_msto observers. With timestamp-based notifications, this is no longer needed.@timingand@clean_timinginstance variables fromTaskWrapper.Layout::Base::TaskState— RemoveThe current
TaskStateclass inLayout::Base(tracksrun_state,clean_state,run_duration,clean_duration,run_error,clean_errorper task) is removed. With the new design:task_updatednotifications — each observer tracks what it needstimestampin notificationsTaski::Logging), not stored per-taskObservers are responsible for maintaining their own view of task state based on notifications they receive.
Tree Building Code in Layout — Remove
All tree building and dependency analysis code must be removed from Layout classes. This logic is replaced by a single
DependencyGraphbuilt once by ExecutionFacade. Layout observers derive display metadata from the graph inon_ready.Layout::Base— remove the following methods:build_tree_node— recursive tree node construction (replaced bydependency_graph)get_task_dependencies— callsStaticAnalysis::Analyzer.analyze()(re-runs static analysis)collect_all_dependencies/collect_dependencies_recursive— dependency collectionon_root_task_set— no longer needed (replaced byon_readypulling from Facade)set_root_task— no longer needed (root_task available via Facade Pull)@taskshash (TaskState tracking) — observers track their own stateapply_state_transition— replaced by unifiedtask_updatednotification handlingLayout::Simple— remove:build_tree_structure— orchestrates tree buildingregister_tasks_from_tree— registers tasks from tree into@taskson_root_task_setoverrideLayout::Tree— remove:build_tree_structure— orchestrates tree buildingregister_tree_nodes— registers tree nodes with depth/position metadataon_root_task_setoverridefacade.dependency_graphinon_readyScheduler#build_dependency_graph— RemoveScheduler currently builds its own dependency graph via BFS from
cached_dependencies. This is replaced by pullingdependency_graphfrom ExecutionFacade.Schedulerstate constants — RemoveSTATE_ENQUEUED/CLEAN_STATE_ENQUEUED: No longer needed (unified intorunning)ExecutionContext— Remove (absorbed into ExecutionFacade)All functionality (observer dispatch, output capture management) moves to ExecutionFacade. Individual
notify_*methods are replaced by the unified event catalog.Current methods to remove/merge:
notify_task_registered→ removed (Pull atready)notify_task_started/notify_task_completed/notify_task_skipped→ merged intonotify_task_updatednotify_clean_started/notify_clean_completed→ merged intonotify_task_updatednotify_set_root_task/notify_set_output_capture→ merged intonotify_readynotify_start/notify_stop→ kept asnotify_start/notify_stoptrigger_execution/trigger_clean/execution_trigger=/clean_trigger=→ kept (execution coordination)THREAD_LOCAL_KEY/self.current/self.current=→ updated (Fiber-local context)Implementation Plan
Phase 1: Unify State Constants
STATE_ENQUEUED/CLEAN_STATE_ENQUEUEDfrom Scheduler — unify intorunningLayout::Base#apply_state_transition::cleaning→:running,:clean_completed→:completed,:clean_failed→:failedPhase 2: Introduce ExecutionFacade
ExecutionFacadeclass absorbingExecutionContextDependencyGraphat initialization (single source for all dependency and tree data)root_task_class,dependency_graph,output_stream(all immutable/accumulating)ExecutionContext(replaced by ExecutionFacade)Phase 3: Unify Events
notify_task_registered(available via Pull at ready)notify_task_updated(task_class, previous_state:, current_state:, phase:, timestamp:)notify_set_root_task,notify_set_output_captureintonotify_readynotify_phase_started(phase)/notify_phase_completed(phase)Phase 4: Introduce Observer Base Class
TaskObserverbase classadd_observerPhase 5: Pull-based Observer
TaskObserver@context.dependency_graphinon_readyto derive display metadata (depth,is_last, tree prefixes)TaskStateclass from Layout::Base (see Layout::Base::TaskState — Remove)Scheduler#build_dependency_graph(see Scheduler#build_dependency_graph — Remove)Phase 6: Remove TaskTiming
TaskTimingclass from TaskWrapper@timingand@clean_timinginstance variablestimestamp:from notifications to calculate durationPhase 7: Add skipped State for Unaccessed Tasks
:skippedviatask_updatednotification:skippedPhase 8: Output Processing
OutputHub#read(task_class, limit: nil)methodfacade.output_streamPhase 9: Update Tests
Critical Files
lib/taski/execution/execution_facade.rblib/taski/execution/execution_context.rblib/taski/execution/task_observer.rblib/taski/execution/scheduler.rbbuild_dependency_graph, removeenqueuedstates (pulls from Facade instead)lib/taski/static_analysis/analyzer.rblib/taski/static_analysis/dependency_graph.rblib/taski/progress/layout/base.rblib/taski/progress/layout/simple.rbbuild_tree_structure,register_tasks_from_treelib/taski/progress/layout/tree.rbbuild_tree_structure,register_tree_nodeslib/taski/execution/task_wrapper.rbTaskTiming,@timing,@clean_timing. State holder + state transition triggerlib/taski/execution/executor.rb