aidp 0.34.1 → 0.39.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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/usage_limit.rb +179 -0
  61. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  62. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  63. data/lib/aidp/harness/usage_period.rb +234 -0
  64. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  65. data/lib/aidp/init/runner.rb +25 -4
  66. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  67. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  68. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  69. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  70. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  71. data/lib/aidp/logger.rb +25 -7
  72. data/lib/aidp/orchestration_adapter.rb +279 -0
  73. data/lib/aidp/pr_worktree_manager.rb +4 -15
  74. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  75. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  76. data/lib/aidp/prompts/template_evolver.rb +305 -0
  77. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  78. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  79. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  80. data/lib/aidp/setup/wizard.rb +616 -176
  81. data/lib/aidp/shell_executor.rb +58 -0
  82. data/lib/aidp/style_guide/selector.rb +56 -1
  83. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  84. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  85. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  86. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  87. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  88. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  89. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  90. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  91. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  92. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  93. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  94. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  95. data/lib/aidp/temporal/configuration.rb +169 -0
  96. data/lib/aidp/temporal/connection.rb +93 -0
  97. data/lib/aidp/temporal/worker.rb +119 -0
  98. data/lib/aidp/temporal/workflow_client.rb +171 -0
  99. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  100. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  101. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  102. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  103. data/lib/aidp/temporal.rb +120 -0
  104. data/lib/aidp/version.rb +1 -1
  105. data/lib/aidp/watch/base_processor.rb +35 -0
  106. data/lib/aidp/watch/build_processor.rb +132 -0
  107. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  108. data/lib/aidp/watch/rebase_processor.rb +245 -0
  109. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  110. data/lib/aidp/watch/runner.rb +366 -285
  111. data/lib/aidp/watch/state_store.rb +80 -0
  112. data/lib/aidp/watch/work_item.rb +96 -0
  113. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  114. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  115. data/lib/aidp/workflows/guided_agent.rb +1 -1
  116. data/lib/aidp/worktree.rb +62 -0
  117. data/templates/aidp.yml.example +70 -0
  118. metadata +117 -16
  119. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  120. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  121. data/lib/aidp/providers/adapter.rb +0 -239
  122. data/lib/aidp/providers/aider.rb +0 -264
  123. data/lib/aidp/providers/anthropic.rb +0 -614
  124. data/lib/aidp/providers/base.rb +0 -617
  125. data/lib/aidp/providers/capability_registry.rb +0 -204
  126. data/lib/aidp/providers/codex.rb +0 -267
  127. data/lib/aidp/providers/cursor.rb +0 -304
  128. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  129. data/lib/aidp/providers/gemini.rb +0 -110
  130. data/lib/aidp/providers/github_copilot.rb +0 -227
  131. data/lib/aidp/providers/kilocode.rb +0 -157
  132. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "yaml"
5
+
6
+ module Aidp
7
+ module Temporal
8
+ # Configuration management for Temporal integration
9
+ # Loads settings from aidp.yml and environment variables
10
+ class Configuration
11
+ DEFAULT_CONFIG = {
12
+ enabled: false, # Disabled by default - users must explicitly opt-in
13
+ target_host: "localhost:7233",
14
+ namespace: "default",
15
+ task_queue: "aidp-workflows",
16
+ tls: false,
17
+ worker: {
18
+ max_concurrent_activities: 10,
19
+ max_concurrent_workflows: 10,
20
+ shutdown_grace_period: 30
21
+ },
22
+ timeouts: {
23
+ workflow_execution: 86400, # 24 hours
24
+ workflow_run: 3600, # 1 hour
25
+ activity_start_to_close: 600, # 10 minutes
26
+ activity_schedule_to_close: 1800, # 30 minutes
27
+ activity_heartbeat: 60 # 1 minute
28
+ },
29
+ retry: {
30
+ initial_interval: 1,
31
+ backoff_coefficient: 2.0,
32
+ maximum_interval: 60,
33
+ maximum_attempts: 3
34
+ }
35
+ }.freeze
36
+
37
+ attr_reader :config
38
+
39
+ def initialize(project_dir = Dir.pwd)
40
+ @project_dir = project_dir
41
+ @config = load_config
42
+ end
43
+
44
+ # Check if Temporal is enabled
45
+ def enabled?
46
+ @config[:enabled]
47
+ end
48
+
49
+ # Get connection configuration
50
+ def connection_config
51
+ {
52
+ target_host: @config[:target_host],
53
+ namespace: @config[:namespace],
54
+ tls: @config[:tls],
55
+ api_key: @config[:api_key]
56
+ }
57
+ end
58
+
59
+ # Get worker configuration
60
+ def worker_config
61
+ {
62
+ task_queue: @config[:task_queue],
63
+ max_concurrent_activities: @config.dig(:worker, :max_concurrent_activities),
64
+ max_concurrent_workflows: @config.dig(:worker, :max_concurrent_workflows),
65
+ shutdown_grace_period: @config.dig(:worker, :shutdown_grace_period)
66
+ }
67
+ end
68
+
69
+ # Get timeout configuration
70
+ def timeout_config
71
+ @config[:timeouts] || {}
72
+ end
73
+
74
+ # Get retry policy configuration
75
+ def retry_config
76
+ @config[:retry] || {}
77
+ end
78
+
79
+ # Get task queue name
80
+ def task_queue
81
+ @config[:task_queue]
82
+ end
83
+
84
+ # Get target host
85
+ def target_host
86
+ @config[:target_host]
87
+ end
88
+
89
+ # Get namespace
90
+ def namespace
91
+ @config[:namespace]
92
+ end
93
+
94
+ # Build a connection with this configuration
95
+ def build_connection
96
+ Connection.new(connection_config)
97
+ end
98
+
99
+ # Build a worker with this configuration
100
+ def build_worker(connection: nil)
101
+ conn = connection || build_connection
102
+ Worker.new(connection: conn, config: worker_config)
103
+ end
104
+
105
+ # Build a workflow client with this configuration
106
+ def build_workflow_client(connection: nil)
107
+ conn = connection || build_connection
108
+ WorkflowClient.new(connection: conn)
109
+ end
110
+
111
+ private
112
+
113
+ def load_config
114
+ base_config = deep_merge(DEFAULT_CONFIG.dup, load_yaml_config)
115
+ apply_environment_overrides(base_config)
116
+ end
117
+
118
+ def load_yaml_config
119
+ config_path = File.join(@project_dir, ".aidp", "aidp.yml")
120
+ return {} unless File.exist?(config_path)
121
+
122
+ full_config = YAML.safe_load_file(config_path, permitted_classes: [Date, Time, Symbol], aliases: true)
123
+ temporal_config = full_config["temporal"] || full_config[:temporal] || {}
124
+
125
+ symbolize_keys(temporal_config)
126
+ rescue => e
127
+ Aidp.log_warn("temporal_config", "load_failed", error: e.message)
128
+ {}
129
+ end
130
+
131
+ def apply_environment_overrides(config)
132
+ # Environment variables take precedence
133
+ config[:target_host] = ENV["TEMPORAL_HOST"] if ENV["TEMPORAL_HOST"]
134
+ config[:namespace] = ENV["TEMPORAL_NAMESPACE"] if ENV["TEMPORAL_NAMESPACE"]
135
+ config[:task_queue] = ENV["TEMPORAL_TASK_QUEUE"] if ENV["TEMPORAL_TASK_QUEUE"]
136
+ config[:api_key] = ENV["TEMPORAL_API_KEY"] if ENV["TEMPORAL_API_KEY"]
137
+ config[:enabled] = ENV["TEMPORAL_ENABLED"] != "false" if ENV.key?("TEMPORAL_ENABLED")
138
+
139
+ if ENV["TEMPORAL_TLS"]
140
+ config[:tls] = ENV["TEMPORAL_TLS"] == "true"
141
+ end
142
+
143
+ config
144
+ end
145
+
146
+ def deep_merge(base, override)
147
+ return base if override.nil? || override.empty?
148
+
149
+ base.merge(override) do |_key, base_val, override_val|
150
+ if base_val.is_a?(Hash) && override_val.is_a?(Hash)
151
+ deep_merge(base_val, override_val)
152
+ else
153
+ override_val.nil? ? base_val : override_val
154
+ end
155
+ end
156
+ end
157
+
158
+ def symbolize_keys(hash)
159
+ return hash unless hash.is_a?(Hash)
160
+
161
+ hash.each_with_object({}) do |(key, value), result|
162
+ new_key = key.is_a?(String) ? key.to_sym : key
163
+ new_value = value.is_a?(Hash) ? symbolize_keys(value) : value
164
+ result[new_key] = new_value
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "temporalio/client"
4
+
5
+ module Aidp
6
+ module Temporal
7
+ # Manages Temporal client connections
8
+ # Handles connection pooling, configuration, and lifecycle
9
+ class Connection
10
+ DEFAULT_TARGET_HOST = "localhost:7233"
11
+ DEFAULT_NAMESPACE = "default"
12
+
13
+ attr_reader :client, :config
14
+
15
+ def initialize(config = {})
16
+ @config = normalize_config(config)
17
+ @client = nil
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ # Get or create a connected client
22
+ # Thread-safe lazy initialization
23
+ def connect
24
+ @mutex.synchronize do
25
+ @client ||= create_client
26
+ end
27
+ end
28
+
29
+ # Check if connected
30
+ def connected?
31
+ @mutex.synchronize { !@client.nil? }
32
+ end
33
+
34
+ # Close the connection
35
+ def close
36
+ @mutex.synchronize do
37
+ @client = nil
38
+ end
39
+ end
40
+
41
+ # Get target host
42
+ def target_host
43
+ @config[:target_host]
44
+ end
45
+
46
+ # Get namespace
47
+ def namespace
48
+ @config[:namespace]
49
+ end
50
+
51
+ private
52
+
53
+ def normalize_config(config)
54
+ {
55
+ target_host: config[:target_host] || config["target_host"] || ENV["TEMPORAL_HOST"] || DEFAULT_TARGET_HOST,
56
+ namespace: config[:namespace] || config["namespace"] || ENV["TEMPORAL_NAMESPACE"] || DEFAULT_NAMESPACE,
57
+ tls: config[:tls] || config["tls"] || false,
58
+ api_key: config[:api_key] || config["api_key"] || ENV["TEMPORAL_API_KEY"]
59
+ }
60
+ end
61
+
62
+ def create_client
63
+ Aidp.log_debug("temporal_connection", "connecting",
64
+ target_host: @config[:target_host],
65
+ namespace: @config[:namespace],
66
+ tls: @config[:tls])
67
+
68
+ options = {
69
+ target_host: @config[:target_host],
70
+ namespace: @config[:namespace]
71
+ }
72
+
73
+ # Add TLS configuration if enabled
74
+ if @config[:tls]
75
+ options[:tls] = true
76
+ end
77
+
78
+ # Add API key if provided (for Temporal Cloud)
79
+ if @config[:api_key]
80
+ options[:api_key] = @config[:api_key]
81
+ end
82
+
83
+ client = Temporalio::Client.connect(**options)
84
+
85
+ Aidp.log_info("temporal_connection", "connected",
86
+ target_host: @config[:target_host],
87
+ namespace: @config[:namespace])
88
+
89
+ client
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "temporalio/worker"
4
+ require_relative "connection"
5
+
6
+ module Aidp
7
+ module Temporal
8
+ # Temporal worker that runs workflows and activities
9
+ # Manages the worker lifecycle and task queue configuration
10
+ class Worker
11
+ DEFAULT_TASK_QUEUE = "aidp-workflows"
12
+ DEFAULT_MAX_CONCURRENT_ACTIVITIES = 10
13
+ DEFAULT_MAX_CONCURRENT_WORKFLOWS = 10
14
+
15
+ attr_reader :task_queue, :config, :connection
16
+
17
+ def initialize(connection:, config: {})
18
+ @connection = connection
19
+ @config = normalize_config(config)
20
+ @task_queue = @config[:task_queue]
21
+ @worker = nil
22
+ @running = false
23
+ @shutdown_requested = false
24
+ end
25
+
26
+ # Register workflow classes
27
+ def register_workflows(*workflow_classes)
28
+ @workflow_classes ||= []
29
+ @workflow_classes.concat(workflow_classes)
30
+ self
31
+ end
32
+
33
+ # Register activity classes or instances
34
+ def register_activities(*activity_classes)
35
+ @activity_classes ||= []
36
+ @activity_classes.concat(activity_classes)
37
+ self
38
+ end
39
+
40
+ # Start the worker (blocking)
41
+ def run
42
+ return if @running
43
+
44
+ Aidp.log_info("temporal_worker", "starting",
45
+ task_queue: @task_queue,
46
+ workflows: @workflow_classes&.map(&:name),
47
+ activities: @activity_classes&.map { |a| a.is_a?(Class) ? a.name : a.class.name })
48
+
49
+ @running = true
50
+ @shutdown_requested = false
51
+
52
+ begin
53
+ @worker = create_worker
54
+ @worker.run
55
+ rescue => e
56
+ Aidp.log_error("temporal_worker", "run_failed",
57
+ error: e.message,
58
+ error_class: e.class.name)
59
+ raise
60
+ ensure
61
+ @running = false
62
+ @worker = nil
63
+ end
64
+ end
65
+
66
+ # Request graceful shutdown
67
+ def shutdown
68
+ return unless @running
69
+
70
+ Aidp.log_info("temporal_worker", "shutdown_requested", task_queue: @task_queue)
71
+ @shutdown_requested = true
72
+ @worker&.shutdown
73
+ end
74
+
75
+ # Check if running
76
+ def running?
77
+ @running
78
+ end
79
+
80
+ # Check if shutdown was requested
81
+ def shutdown_requested?
82
+ @shutdown_requested
83
+ end
84
+
85
+ private
86
+
87
+ def normalize_config(config)
88
+ {
89
+ task_queue: config[:task_queue] || config["task_queue"] || ENV["TEMPORAL_TASK_QUEUE"] || DEFAULT_TASK_QUEUE,
90
+ max_concurrent_activities: config[:max_concurrent_activities] || config["max_concurrent_activities"] || DEFAULT_MAX_CONCURRENT_ACTIVITIES,
91
+ max_concurrent_workflows: config[:max_concurrent_workflows] || config["max_concurrent_workflows"] || DEFAULT_MAX_CONCURRENT_WORKFLOWS
92
+ }
93
+ end
94
+
95
+ def create_worker
96
+ client = @connection.connect
97
+
98
+ worker_options = {
99
+ client: client,
100
+ task_queue: @task_queue
101
+ }
102
+
103
+ # Add workflows if registered
104
+ worker_options[:workflows] = @workflow_classes if @workflow_classes&.any?
105
+
106
+ # Add activities if registered
107
+ if @activity_classes&.any?
108
+ worker_options[:activities] = @activity_classes
109
+ end
110
+
111
+ # Add tuning options
112
+ worker_options[:max_concurrent_activity_task_polls] = @config[:max_concurrent_activities]
113
+ worker_options[:max_concurrent_workflow_task_polls] = @config[:max_concurrent_workflows]
114
+
115
+ Temporalio::Worker.new(**worker_options)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "connection"
4
+
5
+ module Aidp
6
+ module Temporal
7
+ # Client interface for starting and managing Temporal workflows
8
+ # Wraps Temporal client operations with AIDP-specific conventions
9
+ class WorkflowClient
10
+ DEFAULT_WORKFLOW_TIMEOUT = 86400 # 24 hours in seconds
11
+ DEFAULT_TASK_QUEUE = "aidp-workflows"
12
+
13
+ attr_reader :connection
14
+
15
+ def initialize(connection:)
16
+ @connection = connection
17
+ end
18
+
19
+ # Start a workflow execution
20
+ # Returns workflow handle for status tracking
21
+ def start_workflow(workflow_class, input, options = {})
22
+ client = @connection.connect
23
+ workflow_id = options[:workflow_id] || generate_workflow_id(workflow_class)
24
+ task_queue = options[:task_queue] || DEFAULT_TASK_QUEUE
25
+
26
+ Aidp.log_info("workflow_client", "starting_workflow",
27
+ workflow: workflow_class.name,
28
+ workflow_id: workflow_id,
29
+ task_queue: task_queue)
30
+
31
+ handle = client.start_workflow(
32
+ workflow_class,
33
+ input,
34
+ id: workflow_id,
35
+ task_queue: task_queue,
36
+ execution_timeout: options[:execution_timeout] || DEFAULT_WORKFLOW_TIMEOUT,
37
+ **extract_temporal_options(options)
38
+ )
39
+
40
+ Aidp.log_debug("workflow_client", "workflow_started",
41
+ workflow_id: workflow_id,
42
+ run_id: handle.result_run_id)
43
+
44
+ handle
45
+ end
46
+
47
+ # Execute a workflow synchronously (start and wait for result)
48
+ def execute_workflow(workflow_class, input, options = {})
49
+ handle = start_workflow(workflow_class, input, options)
50
+ handle.result
51
+ end
52
+
53
+ # Get a workflow handle by ID
54
+ def get_workflow(workflow_id, run_id: nil)
55
+ client = @connection.connect
56
+ client.workflow_handle(workflow_id, run_id: run_id)
57
+ end
58
+
59
+ # Query workflow state
60
+ def query_workflow(workflow_id, query_name, *args)
61
+ handle = get_workflow(workflow_id)
62
+ handle.query(query_name, *args)
63
+ end
64
+
65
+ # Signal a running workflow
66
+ def signal_workflow(workflow_id, signal_name, *args)
67
+ handle = get_workflow(workflow_id)
68
+
69
+ Aidp.log_debug("workflow_client", "signaling_workflow",
70
+ workflow_id: workflow_id,
71
+ signal: signal_name)
72
+
73
+ handle.signal(signal_name, *args)
74
+ end
75
+
76
+ # Cancel a running workflow
77
+ def cancel_workflow(workflow_id)
78
+ handle = get_workflow(workflow_id)
79
+
80
+ Aidp.log_info("workflow_client", "canceling_workflow",
81
+ workflow_id: workflow_id)
82
+
83
+ handle.cancel
84
+ end
85
+
86
+ # Terminate a workflow
87
+ def terminate_workflow(workflow_id, reason: nil)
88
+ handle = get_workflow(workflow_id)
89
+
90
+ Aidp.log_info("workflow_client", "terminating_workflow",
91
+ workflow_id: workflow_id,
92
+ reason: reason)
93
+
94
+ handle.terminate(reason)
95
+ end
96
+
97
+ # Get workflow result (waits for completion)
98
+ def get_workflow_result(workflow_id, timeout: nil)
99
+ handle = get_workflow(workflow_id)
100
+
101
+ if timeout
102
+ # Use timeout for waiting
103
+ Timeout.timeout(timeout) do
104
+ handle.result
105
+ end
106
+ else
107
+ handle.result
108
+ end
109
+ end
110
+
111
+ # Check if workflow is running
112
+ def workflow_running?(workflow_id)
113
+ handle = get_workflow(workflow_id)
114
+ desc = handle.describe
115
+ desc.status == :running
116
+ rescue Temporalio::Error::RPCError => e
117
+ # NOT_FOUND means workflow doesn't exist
118
+ return false if e.code == Temporalio::Error::RPCError::Code::NOT_FOUND
119
+
120
+ raise
121
+ end
122
+
123
+ # List workflows with optional filters
124
+ def list_workflows(query: nil, page_size: 100)
125
+ client = @connection.connect
126
+
127
+ options = {page_size: page_size}
128
+ options[:query] = query if query
129
+
130
+ client.list_workflows(**options)
131
+ end
132
+
133
+ private
134
+
135
+ def generate_workflow_id(workflow_class)
136
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
137
+ random = SecureRandom.hex(4)
138
+ workflow_name = workflow_class.name.split("::").last.gsub(/Workflow$/, "").downcase
139
+ "#{workflow_name}_#{timestamp}_#{random}"
140
+ end
141
+
142
+ def extract_temporal_options(options)
143
+ temporal_opts = {}
144
+
145
+ # Timeout options
146
+ temporal_opts[:run_timeout] = options[:run_timeout] if options[:run_timeout]
147
+ temporal_opts[:task_timeout] = options[:task_timeout] if options[:task_timeout]
148
+
149
+ # Retry policy
150
+ if options[:retry_policy]
151
+ temporal_opts[:retry_policy] = build_retry_policy(options[:retry_policy])
152
+ end
153
+
154
+ # Memo and search attributes
155
+ temporal_opts[:memo] = options[:memo] if options[:memo]
156
+ temporal_opts[:search_attributes] = options[:search_attributes] if options[:search_attributes]
157
+
158
+ temporal_opts
159
+ end
160
+
161
+ def build_retry_policy(policy_config)
162
+ Temporalio::RetryPolicy.new(
163
+ initial_interval: policy_config[:initial_interval] || 1,
164
+ backoff_coefficient: policy_config[:backoff_coefficient] || 2.0,
165
+ maximum_interval: policy_config[:maximum_interval] || 60,
166
+ maximum_attempts: policy_config[:maximum_attempts] || 3
167
+ )
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "temporalio/workflow"
4
+
5
+ module Aidp
6
+ module Temporal
7
+ module Workflows
8
+ # Base class for AIDP Temporal workflows
9
+ # Provides common patterns and utilities for workflow implementations
10
+ class BaseWorkflow < Temporalio::Workflow::Definition
11
+ # Default activity options applied to all activities
12
+ DEFAULT_ACTIVITY_OPTIONS = {
13
+ start_to_close_timeout: 600, # 10 minutes
14
+ heartbeat_timeout: 60, # 1 minute
15
+ retry_policy: {
16
+ initial_interval: 1,
17
+ backoff_coefficient: 2.0,
18
+ maximum_interval: 60,
19
+ maximum_attempts: 3
20
+ }
21
+ }.freeze
22
+
23
+ class << self
24
+ # Define workflow name from class name
25
+ def workflow_name
26
+ name.split("::").last.gsub(/Workflow$/, "").gsub(/([A-Z])/) { "_#{$1.downcase}" }.sub(/^_/, "")
27
+ end
28
+
29
+ # Helper to define activity with default options
30
+ def activity_options(overrides = {})
31
+ DEFAULT_ACTIVITY_OPTIONS.merge(overrides)
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ # Log within workflow context
38
+ def log_workflow(action, **context)
39
+ workflow_info = Temporalio::Workflow.info
40
+ Aidp.log_debug("temporal_workflow", action,
41
+ workflow_id: workflow_info.workflow_id,
42
+ run_id: workflow_info.run_id,
43
+ **context)
44
+ end
45
+
46
+ # Sleep that respects cancellation
47
+ def workflow_sleep(duration)
48
+ Temporalio::Workflow.sleep(duration)
49
+ end
50
+
51
+ # Get current workflow info
52
+ def workflow_info
53
+ Temporalio::Workflow.info
54
+ end
55
+
56
+ # Check if cancellation was requested
57
+ def cancellation_requested?
58
+ Temporalio::Workflow.cancellation_pending?
59
+ end
60
+
61
+ # Build retry policy from config
62
+ def build_retry_policy(config)
63
+ Temporalio::RetryPolicy.new(
64
+ initial_interval: config[:initial_interval] || 1,
65
+ backoff_coefficient: config[:backoff_coefficient] || 2.0,
66
+ maximum_interval: config[:maximum_interval] || 60,
67
+ maximum_attempts: config[:maximum_attempts] || 3
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end