taski 0.4.2 → 0.7.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +51 -33
  4. data/Steepfile +1 -0
  5. data/docs/GUIDE.md +340 -0
  6. data/examples/README.md +68 -20
  7. data/examples/{context_demo.rb → args_demo.rb} +27 -27
  8. data/examples/clean_demo.rb +204 -0
  9. data/examples/data_pipeline_demo.rb +3 -3
  10. data/examples/group_demo.rb +113 -0
  11. data/examples/nested_section_demo.rb +161 -0
  12. data/examples/parallel_progress_demo.rb +1 -1
  13. data/examples/reexecution_demo.rb +93 -80
  14. data/examples/system_call_demo.rb +56 -0
  15. data/examples/tree_progress_demo.rb +164 -0
  16. data/lib/taski/{context.rb → args.rb} +3 -3
  17. data/lib/taski/execution/execution_context.rb +379 -0
  18. data/lib/taski/execution/executor.rb +538 -0
  19. data/lib/taski/execution/registry.rb +26 -2
  20. data/lib/taski/execution/scheduler.rb +308 -0
  21. data/lib/taski/execution/task_output_pipe.rb +42 -0
  22. data/lib/taski/execution/task_output_router.rb +216 -0
  23. data/lib/taski/execution/task_wrapper.rb +295 -146
  24. data/lib/taski/execution/tree_progress_display.rb +793 -0
  25. data/lib/taski/execution/worker_pool.rb +104 -0
  26. data/lib/taski/section.rb +23 -0
  27. data/lib/taski/static_analysis/analyzer.rb +4 -2
  28. data/lib/taski/static_analysis/visitor.rb +86 -5
  29. data/lib/taski/task.rb +223 -120
  30. data/lib/taski/version.rb +1 -1
  31. data/lib/taski.rb +147 -28
  32. data/sig/taski.rbs +310 -67
  33. metadata +17 -8
  34. data/docs/advanced-features.md +0 -625
  35. data/docs/api-guide.md +0 -509
  36. data/docs/error-handling.md +0 -684
  37. data/lib/taski/execution/coordinator.rb +0 -63
  38. data/lib/taski/execution/parallel_progress_display.rb +0 -201
data/lib/taski/task.rb CHANGED
@@ -2,12 +2,37 @@
2
2
 
3
3
  require_relative "static_analysis/analyzer"
4
4
  require_relative "execution/registry"
5
- require_relative "execution/coordinator"
6
5
  require_relative "execution/task_wrapper"
7
6
 
8
7
  module Taski
8
+ # Base class for all tasks in the Taski framework.
9
+ # Tasks define units of work with dependencies and exported values.
10
+ #
11
+ # @example Defining a simple task
12
+ # class MyTask < Taski::Task
13
+ # exports :result
14
+ #
15
+ # def run
16
+ # @result = "completed"
17
+ # end
18
+ # end
9
19
  class Task
10
20
  class << self
21
+ ##
22
+ # Callback invoked when a subclass is created.
23
+ # Automatically creates a task-specific Error class for each subclass.
24
+ # @param subclass [Class] The newly created subclass.
25
+ def inherited(subclass)
26
+ super
27
+ # Create TaskClass::Error that inherits from Taski::TaskError
28
+ error_class = Class.new(Taski::TaskError)
29
+ subclass.const_set(:Error, error_class)
30
+ end
31
+
32
+ ##
33
+ # Declares exported methods that will be accessible after task execution.
34
+ # Creates instance reader and class accessor methods for each export.
35
+ # @param export_methods [Array<Symbol>] The method names to export.
11
36
  def exports(*export_methods)
12
37
  @exported_methods = export_methods
13
38
 
@@ -17,157 +42,128 @@ module Taski
17
42
  end
18
43
  end
19
44
 
45
+ ##
46
+ # Returns the list of exported method names.
47
+ # @return [Array<Symbol>] The exported method names.
20
48
  def exported_methods
21
49
  @exported_methods ||= []
22
50
  end
23
51
 
24
- # Each call creates a fresh TaskWrapper instance for re-execution support.
52
+ ##
53
+ # Creates a fresh TaskWrapper instance for re-execution support.
25
54
  # Use class methods (e.g., MyTask.result) for cached single execution.
55
+ # @return [Execution::TaskWrapper] A new wrapper for this task.
26
56
  def new
27
- Execution::TaskWrapper.new(
28
- super,
29
- registry: registry,
30
- coordinator: coordinator
31
- )
57
+ fresh_wrapper
32
58
  end
33
59
 
60
+ ##
61
+ # Returns cached static dependencies for this task class.
62
+ # Dependencies are analyzed from the run method body using static analysis.
63
+ # @return [Set<Class>] The set of task classes this task depends on.
34
64
  def cached_dependencies
35
65
  @dependencies_cache ||= StaticAnalysis::Analyzer.analyze(self)
36
66
  end
37
67
 
68
+ ##
69
+ # Clears the cached dependency analysis.
70
+ # Useful when task code has changed and dependencies need to be re-analyzed.
38
71
  def clear_dependency_cache
39
72
  @dependencies_cache = nil
40
73
  end
41
74
 
42
- def run(context: {})
43
- Taski.start_context(options: context, root_task: self)
75
+ ##
76
+ # Executes the task and all its dependencies.
77
+ # Creates a fresh registry each time for independent execution.
78
+ # @param args [Hash] User-defined arguments accessible via Taski.args.
79
+ # @param workers [Integer, nil] Number of worker threads for parallel execution.
80
+ # Must be a positive integer or nil.
81
+ # Use workers: 1 for sequential execution (useful for debugging).
82
+ # @raise [ArgumentError] If workers is not a positive integer or nil.
83
+ # @return [Object] The result of task execution.
84
+ def run(args: {}, workers: nil)
85
+ validate_workers!(workers)
86
+ Taski.start_args(options: args.merge(_workers: workers), root_task: self)
44
87
  validate_no_circular_dependencies!
45
- cached_wrapper.run
88
+ fresh_wrapper.run
89
+ ensure
90
+ Taski.reset_args!
46
91
  end
47
92
 
48
- def clean(context: {})
49
- Taski.start_context(options: context, root_task: self)
93
+ ##
94
+ # Executes the clean phase for the task and all its dependencies.
95
+ # Clean is executed in reverse dependency order.
96
+ # Creates a fresh registry each time for independent execution.
97
+ # @param args [Hash] User-defined arguments accessible via Taski.args.
98
+ # @param workers [Integer, nil] Number of worker threads for parallel execution.
99
+ # Must be a positive integer or nil.
100
+ # @raise [ArgumentError] If workers is not a positive integer or nil.
101
+ def clean(args: {}, workers: nil)
102
+ validate_workers!(workers)
103
+ Taski.start_args(options: args.merge(_workers: workers), root_task: self)
50
104
  validate_no_circular_dependencies!
51
- cached_wrapper.clean
105
+ fresh_wrapper.clean
106
+ ensure
107
+ Taski.reset_args!
52
108
  end
53
109
 
54
- def registry
55
- Taski.global_registry
56
- end
57
-
58
- def coordinator
59
- @coordinator ||= Execution::Coordinator.new(
60
- registry: registry,
61
- analyzer: StaticAnalysis::Analyzer
62
- )
110
+ ##
111
+ # Execute run followed by clean in a single operation.
112
+ # If run fails, clean is still executed for resource release.
113
+ # Creates a fresh registry for both operations to share.
114
+ #
115
+ # @param args [Hash] User-defined arguments accessible via Taski.args.
116
+ # @param workers [Integer, nil] Number of worker threads for parallel execution.
117
+ # Must be a positive integer or nil.
118
+ # @raise [ArgumentError] If workers is not a positive integer or nil.
119
+ # @return [Object] The result of task execution
120
+ def run_and_clean(args: {}, workers: nil)
121
+ validate_workers!(workers)
122
+ Taski.start_args(options: args.merge(_workers: workers), root_task: self)
123
+ validate_no_circular_dependencies!
124
+ fresh_wrapper.run_and_clean
125
+ ensure
126
+ Taski.reset_args!
63
127
  end
64
128
 
129
+ ##
130
+ # Resets the task state and progress display.
131
+ # Useful for testing or re-running tasks from scratch.
65
132
  def reset!
66
- registry.reset!
67
- Taski.reset_global_registry!
68
- Taski.reset_context!
69
- @coordinator = nil
133
+ Taski.reset_args!
134
+ Taski.reset_progress_display!
70
135
  @circular_dependency_checked = false
71
136
  end
72
137
 
138
+ ##
139
+ # Renders a static tree representation of the task dependencies.
140
+ # @return [String] The rendered tree string.
73
141
  def tree
74
- build_tree(self, "", {}, false)
142
+ Execution::TreeProgressDisplay.render_static_tree(self)
75
143
  end
76
144
 
77
145
  private
78
146
 
79
- # ANSI color codes
80
- COLORS = {
81
- reset: "\e[0m",
82
- task: "\e[32m", # green
83
- section: "\e[34m", # blue
84
- impl: "\e[33m", # yellow
85
- tree: "\e[90m", # gray
86
- name: "\e[1m" # bold
87
- }.freeze
88
-
89
- def build_tree(task_class, prefix, task_index_map, is_impl, ancestors = Set.new)
90
- type_label = colored_type_label(task_class)
91
- impl_prefix = is_impl ? "#{COLORS[:impl]}[impl]#{COLORS[:reset]} " : ""
92
- task_number = get_task_number(task_class, task_index_map)
93
- name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
94
-
95
- # Detect circular reference
96
- if ancestors.include?(task_class)
97
- circular_marker = "#{COLORS[:impl]}(circular)#{COLORS[:reset]}"
98
- return "#{impl_prefix}#{task_number} #{name} #{type_label} #{circular_marker}\n"
99
- end
100
-
101
- result = "#{impl_prefix}#{task_number} #{name} #{type_label}\n"
102
-
103
- # Register task number if not already registered
104
- task_index_map[task_class] = task_index_map.size + 1 unless task_index_map.key?(task_class)
105
-
106
- # Add to ancestors for circular detection
107
- new_ancestors = ancestors + [task_class]
108
-
109
- # Use static analysis to include Section.impl candidates for visualization
110
- dependencies = StaticAnalysis::Analyzer.analyze(task_class).to_a
111
- is_section = section_class?(task_class)
112
-
113
- dependencies.each_with_index do |dep, index|
114
- is_last = (index == dependencies.size - 1)
115
- result += format_dependency_branch(dep, prefix, is_last, task_index_map, is_section, new_ancestors)
116
- end
117
-
118
- result
119
- end
120
-
121
- def format_dependency_branch(dep, prefix, is_last, task_index_map, is_impl, ancestors)
122
- connector, extension = tree_connector_chars(is_last)
123
- dep_tree = build_tree(dep, "#{prefix}#{extension}", task_index_map, is_impl, ancestors)
124
-
125
- result = "#{prefix}#{COLORS[:tree]}#{connector}#{COLORS[:reset]}"
126
- lines = dep_tree.lines
127
- result += lines.first
128
- lines.drop(1).each { |line| result += line }
129
- result
130
- end
131
-
132
- def tree_connector_chars(is_last)
133
- if is_last
134
- ["└── ", " "]
135
- else
136
- ["├── ", "│ "]
137
- end
138
- end
139
-
140
- def get_task_number(task_class, task_index_map)
141
- number = task_index_map[task_class] || (task_index_map.size + 1)
142
- "#{COLORS[:tree]}[#{number}]#{COLORS[:reset]}"
143
- end
144
-
145
- def colored_type_label(klass)
146
- if section_class?(klass)
147
- "#{COLORS[:section]}(Section)#{COLORS[:reset]}"
148
- else
149
- "#{COLORS[:task]}(Task)#{COLORS[:reset]}"
150
- end
151
- end
152
-
153
- def section_class?(klass)
154
- defined?(Taski::Section) && klass < Taski::Section
155
- end
156
-
157
- # Use allocate + initialize instead of new to avoid infinite loop
158
- # since new is overridden to return TaskWrapper
159
- def cached_wrapper
160
- registry.get_or_create(self) do
161
- task_instance = allocate
162
- task_instance.send(:initialize)
163
- Execution::TaskWrapper.new(
164
- task_instance,
165
- registry: registry,
166
- coordinator: coordinator
167
- )
168
- end
147
+ ##
148
+ # Creates a fresh TaskWrapper with its own registry.
149
+ # Used for class method execution (Task.run) where each call is independent.
150
+ # @return [Execution::TaskWrapper] A new wrapper with fresh registry.
151
+ def fresh_wrapper
152
+ fresh_registry = Execution::Registry.new
153
+ task_instance = allocate
154
+ task_instance.__send__(:initialize)
155
+ wrapper = Execution::TaskWrapper.new(
156
+ task_instance,
157
+ registry: fresh_registry,
158
+ execution_context: Execution::ExecutionContext.current
159
+ )
160
+ fresh_registry.register(self, wrapper)
161
+ wrapper
169
162
  end
170
163
 
164
+ ##
165
+ # Defines an instance reader method for an exported value.
166
+ # @param method [Symbol] The method name to define.
171
167
  def define_instance_reader(method)
172
168
  undef_method(method) if method_defined?(method)
173
169
 
@@ -177,16 +173,48 @@ module Taski
177
173
  end
178
174
  end
179
175
 
176
+ ##
177
+ # Defines a class accessor method for an exported value.
178
+ # When called inside an execution, returns cached value from registry.
179
+ # When called outside execution, creates fresh execution.
180
+ # @param method [Symbol] The method name to define.
180
181
  def define_class_accessor(method)
181
182
  singleton_class.undef_method(method) if singleton_class.method_defined?(method)
182
183
 
183
184
  define_singleton_method(method) do
184
- Taski.start_context(options: {}, root_task: self)
185
- validate_no_circular_dependencies!
186
- cached_wrapper.get_exported_value(method)
185
+ # Check if running inside an execution with a registry
186
+ registry = Taski.current_registry
187
+ if registry
188
+ # Inside execution - get or create wrapper in current registry
189
+ # This handles both pre-registered dependencies and dynamic ones (like Section impl)
190
+ wrapper = registry.get_or_create(self) do
191
+ task_instance = allocate
192
+ task_instance.__send__(:initialize)
193
+ Execution::TaskWrapper.new(
194
+ task_instance,
195
+ registry: registry,
196
+ execution_context: Execution::ExecutionContext.current
197
+ )
198
+ end
199
+ wrapper.get_exported_value(method)
200
+ else
201
+ # Outside execution - fresh execution (top-level call)
202
+ args_was_nil = Taski.args.nil?
203
+ begin
204
+ Taski.start_args(options: {}, root_task: self) if args_was_nil
205
+ validate_no_circular_dependencies!
206
+ fresh_wrapper.get_exported_value(method)
207
+ ensure
208
+ # Only reset if we set args
209
+ Taski.reset_args! if args_was_nil
210
+ end
211
+ end
187
212
  end
188
213
  end
189
214
 
215
+ ##
216
+ # Validates that no circular dependencies exist in the task graph.
217
+ # @raise [Taski::CircularDependencyError] If circular dependencies are detected.
190
218
  def validate_no_circular_dependencies!
191
219
  return if @circular_dependency_checked
192
220
 
@@ -199,15 +227,90 @@ module Taski
199
227
 
200
228
  @circular_dependency_checked = true
201
229
  end
230
+
231
+ ##
232
+ # Validates the workers parameter.
233
+ # @param workers [Object] The workers parameter to validate.
234
+ # @raise [ArgumentError] If workers is not a positive integer or nil.
235
+ def validate_workers!(workers)
236
+ return if workers.nil?
237
+
238
+ unless workers.is_a?(Integer) && workers >= 1
239
+ raise ArgumentError, "workers must be a positive integer or nil, got: #{workers.inspect}"
240
+ end
241
+ end
202
242
  end
203
243
 
244
+ ##
245
+ # Executes the task's main logic.
246
+ # Subclasses must override this method to implement task behavior.
247
+ # @raise [NotImplementedError] If not overridden in a subclass.
204
248
  def run
205
249
  raise NotImplementedError, "Subclasses must implement the run method"
206
250
  end
207
251
 
252
+ ##
253
+ # Cleans up resources after task execution.
254
+ # Override in subclasses to implement cleanup logic.
208
255
  def clean
209
256
  end
210
257
 
258
+ # Override system() to capture subprocess output through the pipe-based architecture.
259
+ # Uses Kernel.system with :out option to redirect output to the task's pipe.
260
+ # If user provides :out or :err options, they are respected (no automatic redirection).
261
+ # @param args [Array] Command arguments (shell mode if single string, exec mode if array)
262
+ # @param opts [Hash] Options passed to Kernel.system
263
+ # @return [Boolean, nil] true if command succeeded, false if failed, nil if command not found
264
+ def system(*args, **opts)
265
+ write_io = $stdout.respond_to?(:current_write_io) ? $stdout.current_write_io : nil
266
+
267
+ if write_io && !opts.key?(:out)
268
+ # Redirect subprocess output to the task's pipe (stderr merged into stdout)
269
+ Kernel.system(*args, out: write_io, err: [:child, :out], **opts)
270
+ else
271
+ # No capture active or user provided custom :out, use normal system
272
+ Kernel.system(*args, **opts)
273
+ end
274
+ end
275
+
276
+ # Groups related output within a task for organized progress display.
277
+ # The group name is shown in the progress tree as a child of the task.
278
+ # Groups cannot be nested.
279
+ #
280
+ # @param name [String] The group name to display
281
+ # @yield The block to execute within the group
282
+ # @return [Object] The result of the block
283
+ # @raise Re-raises any exception from the block after marking group as failed
284
+ #
285
+ # @example
286
+ # def run
287
+ # group("Preparing") do
288
+ # puts "Checking dependencies..."
289
+ # puts "Validating config..."
290
+ # end
291
+ # group("Deploying") do
292
+ # puts "Uploading files..."
293
+ # end
294
+ # end
295
+ def group(name)
296
+ context = Execution::ExecutionContext.current
297
+ context&.notify_group_started(self.class, name)
298
+ start_time = Time.now
299
+
300
+ begin
301
+ result = yield
302
+ duration_ms = ((Time.now - start_time) * 1000).round(0)
303
+ context&.notify_group_completed(self.class, name, duration: duration_ms)
304
+ result
305
+ rescue => e
306
+ duration_ms = ((Time.now - start_time) * 1000).round(0)
307
+ context&.notify_group_completed(self.class, name, duration: duration_ms, error: e)
308
+ raise
309
+ end
310
+ end
311
+
312
+ ##
313
+ # Resets the instance's exported values to nil.
211
314
  def reset!
212
315
  self.class.exported_methods.each do |method|
213
316
  instance_variable_set("@#{method}", nil)
data/lib/taski/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taski
4
- VERSION = "0.4.2"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/taski.rb CHANGED
@@ -5,12 +5,13 @@ require_relative "taski/static_analysis/analyzer"
5
5
  require_relative "taski/static_analysis/visitor"
6
6
  require_relative "taski/static_analysis/dependency_graph"
7
7
  require_relative "taski/execution/registry"
8
- require_relative "taski/execution/coordinator"
8
+ require_relative "taski/execution/execution_context"
9
9
  require_relative "taski/execution/task_wrapper"
10
- require_relative "taski/execution/parallel_progress_display"
11
- require_relative "taski/context"
12
- require_relative "taski/task"
13
- require_relative "taski/section"
10
+ require_relative "taski/execution/scheduler"
11
+ require_relative "taski/execution/worker_pool"
12
+ require_relative "taski/execution/executor"
13
+ require_relative "taski/execution/tree_progress_display"
14
+ require_relative "taski/args"
14
15
 
15
16
  module Taski
16
17
  class TaskAbortException < StandardError
@@ -28,48 +29,166 @@ module Taski
28
29
  end
29
30
  end
30
31
 
31
- @context_monitor = Monitor.new
32
+ # Represents a single task failure with its context
33
+ class TaskFailure
34
+ attr_reader :task_class, :error
32
35
 
33
- # Get the current execution context
34
- # @return [Context, nil] The current context or nil if no task is running
35
- def self.context
36
- @context_monitor.synchronize { @context }
36
+ # @param task_class [Class] The task class that failed
37
+ # @param error [Exception] The exception that was raised
38
+ def initialize(task_class:, error:)
39
+ @task_class = task_class
40
+ @error = error
41
+ end
37
42
  end
38
43
 
39
- # Start a new execution context (internal use only)
40
- # @api private
41
- def self.start_context(options:, root_task:)
42
- @context_monitor.synchronize do
43
- return if @context
44
- @context = Context.new(options: options, root_task: root_task)
44
+ # Mixin for exception classes to enable transparent rescue matching with AggregateError.
45
+ # When extended by an exception class, `rescue ThatError` will also match
46
+ # an AggregateError that contains ThatError.
47
+ #
48
+ # @note TaskError and all TaskClass::Error classes already extend this module.
49
+ #
50
+ # @example
51
+ # begin
52
+ # MyTask.value # raises AggregateError containing MyTask::Error
53
+ # rescue MyTask::Error => e
54
+ # puts "MyTask failed: #{e.message}"
55
+ # end
56
+ module AggregateAware
57
+ def ===(other)
58
+ return super unless other.is_a?(Taski::AggregateError)
59
+
60
+ other.includes?(self)
45
61
  end
46
62
  end
47
63
 
48
- # Reset the execution context (internal use only)
49
- # @api private
50
- def self.reset_context!
51
- @context_monitor.synchronize { @context = nil }
64
+ # Base class for task-specific error wrappers.
65
+ # Each Task subclass automatically gets a ::Error class that inherits from this.
66
+ # This allows rescuing errors by task class: rescue MyTask::Error => e
67
+ #
68
+ # @example Rescuing task-specific errors
69
+ # begin
70
+ # MyTask.value
71
+ # rescue MyTask::Error => e
72
+ # puts "MyTask failed: #{e.message}"
73
+ # end
74
+ class TaskError < StandardError
75
+ extend AggregateAware
76
+
77
+ # @return [Exception] The original error that occurred in the task
78
+ attr_reader :cause
79
+
80
+ # @return [Class] The task class where the error occurred
81
+ attr_reader :task_class
82
+
83
+ # @param cause [Exception] The original error
84
+ # @param task_class [Class] The task class where the error occurred
85
+ def initialize(cause, task_class:)
86
+ @cause = cause
87
+ @task_class = task_class
88
+ super(cause.message)
89
+ set_backtrace(cause.backtrace)
90
+ end
91
+ end
92
+
93
+ # Raised when multiple tasks fail during parallel execution
94
+ class AggregateError < StandardError
95
+ attr_reader :errors
96
+
97
+ # @param errors [Array<TaskFailure>] List of task failures
98
+ def initialize(errors)
99
+ @errors = errors
100
+ super(build_message)
101
+ end
102
+
103
+ # Returns the first error for compatibility with exception chaining
104
+ # @return [Exception, nil] The first error or nil if no errors
105
+ def cause
106
+ errors.first&.error
107
+ end
108
+
109
+ # Check if this aggregate contains an error of the given type
110
+ # @param exception_class [Class] The exception class to check for
111
+ # @return [Boolean] true if any contained error is of the given type
112
+ def includes?(exception_class)
113
+ errors.any? { |f| f.error.is_a?(exception_class) }
114
+ end
115
+
116
+ private
117
+
118
+ def build_message
119
+ task_word = (errors.size == 1) ? "task" : "tasks"
120
+ "#{errors.size} #{task_word} failed:\n" +
121
+ errors.map { |f| " - #{f.task_class.name}: #{f.error.message}" }.join("\n")
122
+ end
52
123
  end
53
124
 
54
- def self.global_registry
55
- @global_registry ||= Execution::Registry.new
125
+ @args_monitor = Monitor.new
126
+
127
+ # Get the current runtime arguments
128
+ # @return [Args, nil] The current args or nil if no task is running
129
+ def self.args
130
+ @args_monitor.synchronize { @args }
131
+ end
132
+
133
+ # Start new runtime arguments (internal use only)
134
+ # @api private
135
+ def self.start_args(options:, root_task:)
136
+ @args_monitor.synchronize do
137
+ return if @args
138
+ @args = Args.new(options: options, root_task: root_task)
139
+ end
56
140
  end
57
141
 
58
- def self.reset_global_registry!
59
- @global_registry = nil
142
+ # Reset the runtime arguments (internal use only)
143
+ # @api private
144
+ def self.reset_args!
145
+ @args_monitor.synchronize { @args = nil }
60
146
  end
61
147
 
148
+ # Progress display is enabled by default (tree-style).
149
+ # Environment variables:
150
+ # - TASKI_PROGRESS_DISABLE=1: Disable progress display entirely
62
151
  def self.progress_display
63
- return nil unless progress_enabled?
64
- @progress_display ||= Execution::ParallelProgressDisplay.new
152
+ return nil if progress_disabled?
153
+ @progress_display ||= Execution::TreeProgressDisplay.new
65
154
  end
66
155
 
67
- def self.progress_enabled?
68
- ENV["TASKI_PROGRESS"] == "1" || ENV["TASKI_FORCE_PROGRESS"] == "1"
156
+ def self.progress_disabled?
157
+ ENV["TASKI_PROGRESS_DISABLE"] == "1"
69
158
  end
70
159
 
71
160
  def self.reset_progress_display!
72
161
  @progress_display&.stop
73
162
  @progress_display = nil
74
163
  end
164
+
165
+ # Get the worker count from the current args (set via Task.run(workers: n))
166
+ # @return [Integer, nil] The worker count or nil to use WorkerPool default
167
+ # @api private
168
+ def self.args_worker_count
169
+ args&.fetch(:_workers, nil)
170
+ end
171
+
172
+ # Get the current registry for this thread (used during dependency resolution)
173
+ # @return [Execution::Registry, nil] The current registry or nil
174
+ # @api private
175
+ def self.current_registry
176
+ Thread.current[:taski_current_registry]
177
+ end
178
+
179
+ # Set the current registry for this thread (internal use only)
180
+ # @api private
181
+ def self.set_current_registry(registry)
182
+ Thread.current[:taski_current_registry] = registry
183
+ end
184
+
185
+ # Clear the current registry for this thread (internal use only)
186
+ # @api private
187
+ def self.clear_current_registry
188
+ Thread.current[:taski_current_registry] = nil
189
+ end
75
190
  end
191
+
192
+ # Load Task and Section after Taski module is defined (they depend on TaskError)
193
+ require_relative "taski/task"
194
+ require_relative "taski/section"