igniter 0.5.0 → 0.5.1

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 (657) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -3
  3. data/README.md +162 -624
  4. data/bin/igniter-stack +94 -0
  5. data/docs/README.md +62 -0
  6. data/examples/README.md +74 -349
  7. data/examples/agent_orchestration.rb +76 -0
  8. data/examples/agents.rb +2 -1
  9. data/examples/catalog.rb +412 -0
  10. data/examples/consensus.rb +13 -11
  11. data/examples/dataflow.rb +3 -2
  12. data/examples/distributed_workflow.rb +1 -1
  13. data/examples/effects.rb +10 -9
  14. data/examples/incremental.rb +4 -3
  15. data/examples/introspection.rb +49 -0
  16. data/examples/invariants.rb +4 -3
  17. data/examples/llm_tools.rb +18 -18
  18. data/examples/mesh.rb +16 -14
  19. data/examples/mesh_discovery.rb +41 -21
  20. data/examples/mesh_gossip.rb +19 -17
  21. data/examples/reactive_auditing.rb +50 -0
  22. data/examples/run.rb +163 -0
  23. data/lib/igniter/monorepo_packages.rb +17 -0
  24. data/lib/igniter/stack.rb +5 -0
  25. data/lib/igniter.rb +33 -17
  26. data/packages/igniter-agents/README.md +22 -0
  27. data/{lib → packages/igniter-agents/lib}/igniter/agent/ref.rb +1 -0
  28. data/{lib → packages/igniter-agents/lib}/igniter/agent/runner.rb +12 -0
  29. data/{lib → packages/igniter-agents/lib}/igniter/agent.rb +6 -0
  30. data/{lib → packages/igniter-agents/lib}/igniter/agents/proactive_agent.rb +1 -1
  31. data/packages/igniter-agents/lib/igniter/agents.rb +23 -0
  32. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/chain_agent.rb +4 -2
  33. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/critic_agent.rb +4 -2
  34. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/evaluator_agent.rb +4 -2
  35. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/evolution_agent.rb +4 -2
  36. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/observer_agent.rb +4 -2
  37. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/planner_agent.rb +4 -2
  38. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/router_agent.rb +4 -2
  39. data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/ai/agents}/self_reflection_agent.rb +4 -2
  40. data/packages/igniter-agents/lib/igniter/ai/agents.rb +25 -0
  41. data/{lib → packages/igniter-agents/lib}/igniter/registry.rb +2 -0
  42. data/packages/igniter-agents/lib/igniter/runtime/registry_agent_adapter.rb +102 -0
  43. data/{lib → packages/igniter-agents/lib}/igniter/supervisor.rb +3 -0
  44. data/packages/igniter-agents/lib/igniter-agents.rb +7 -0
  45. data/packages/igniter-ai/README.md +20 -0
  46. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/config.rb +1 -1
  47. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/context.rb +2 -2
  48. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/executor.rb +14 -14
  49. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/anthropic.rb +5 -5
  50. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/base.rb +1 -1
  51. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/ollama.rb +4 -4
  52. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/openai.rb +5 -5
  53. data/{lib/igniter → packages/igniter-ai/lib/igniter/ai}/skill/feedback.rb +4 -4
  54. data/{lib/igniter → packages/igniter-ai/lib/igniter/ai}/skill/output_schema.rb +6 -6
  55. data/packages/igniter-ai/lib/igniter/ai/skill/runtime_contract.rb +87 -0
  56. data/packages/igniter-ai/lib/igniter/ai/skill.rb +107 -0
  57. data/packages/igniter-ai/lib/igniter/ai/tool_registry.rb +79 -0
  58. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/transcription/providers/assemblyai.rb +8 -8
  59. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/transcription/providers/base.rb +3 -3
  60. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/transcription/providers/deepgram.rb +3 -3
  61. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/transcription/providers/openai.rb +3 -3
  62. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/transcription/transcriber.rb +6 -6
  63. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/transcription/transcript_result.rb +1 -1
  64. data/{lib/igniter/integrations/llm.rb → packages/igniter-ai/lib/igniter/ai.rb} +18 -15
  65. data/packages/igniter-ai/lib/igniter-ai.rb +3 -0
  66. data/packages/igniter-app/README.md +19 -0
  67. data/packages/igniter-app/lib/igniter/app/app_config.rb +43 -0
  68. data/packages/igniter-app/lib/igniter/app/app_host.rb +56 -0
  69. data/packages/igniter-app/lib/igniter/app/app_host_config.rb +27 -0
  70. data/packages/igniter-app/lib/igniter/app/app_host_pack.rb +13 -0
  71. data/{lib/igniter/application → packages/igniter-app/lib/igniter/app}/autoloader.rb +2 -2
  72. data/packages/igniter-app/lib/igniter/app/cluster_app_host.rb +95 -0
  73. data/packages/igniter-app/lib/igniter/app/cluster_app_host_config.rb +78 -0
  74. data/packages/igniter-app/lib/igniter/app/credentials/config_loader.rb +152 -0
  75. data/packages/igniter-app/lib/igniter/app/credentials/credential.rb +48 -0
  76. data/packages/igniter-app/lib/igniter/app/credentials/credential_policy.rb +38 -0
  77. data/packages/igniter-app/lib/igniter/app/credentials/events/credential_event.rb +179 -0
  78. data/packages/igniter-app/lib/igniter/app/credentials/events.rb +12 -0
  79. data/packages/igniter-app/lib/igniter/app/credentials/lease_request.rb +153 -0
  80. data/packages/igniter-app/lib/igniter/app/credentials/policies/ephemeral_lease_policy.rb +35 -0
  81. data/packages/igniter-app/lib/igniter/app/credentials/policies/local_only_policy.rb +33 -0
  82. data/packages/igniter-app/lib/igniter/app/credentials/policies.rb +13 -0
  83. data/packages/igniter-app/lib/igniter/app/credentials/store.rb +21 -0
  84. data/packages/igniter-app/lib/igniter/app/credentials/stores/file_store.rb +114 -0
  85. data/packages/igniter-app/lib/igniter/app/credentials/trail.rb +254 -0
  86. data/packages/igniter-app/lib/igniter/app/credentials.rb +20 -0
  87. data/packages/igniter-app/lib/igniter/app/dev_output_sync.rb +4 -0
  88. data/packages/igniter-app/lib/igniter/app/diagnostics/app_host_contributor.rb +71 -0
  89. data/packages/igniter-app/lib/igniter/app/diagnostics/cluster_app_host_contributor.rb +97 -0
  90. data/packages/igniter-app/lib/igniter/app/diagnostics/credential_contributor.rb +66 -0
  91. data/packages/igniter-app/lib/igniter/app/diagnostics/evolution_contributor.rb +74 -0
  92. data/packages/igniter-app/lib/igniter/app/diagnostics/ignite_contributor.rb +121 -0
  93. data/packages/igniter-app/lib/igniter/app/diagnostics/loader_contributor.rb +68 -0
  94. data/packages/igniter-app/lib/igniter/app/diagnostics/orchestration_contributor.rb +200 -0
  95. data/packages/igniter-app/lib/igniter/app/diagnostics/runtime_contributor.rb +68 -0
  96. data/packages/igniter-app/lib/igniter/app/diagnostics/scheduler_contributor.rb +72 -0
  97. data/packages/igniter-app/lib/igniter/app/diagnostics/sdk_contributor.rb +284 -0
  98. data/packages/igniter-app/lib/igniter/app/diagnostics.rb +62 -0
  99. data/packages/igniter-app/lib/igniter/app/evolution/approval_decision.rb +115 -0
  100. data/packages/igniter-app/lib/igniter/app/evolution/approval_request.rb +36 -0
  101. data/packages/igniter-app/lib/igniter/app/evolution/plan.rb +72 -0
  102. data/packages/igniter-app/lib/igniter/app/evolution/planner.rb +85 -0
  103. data/packages/igniter-app/lib/igniter/app/evolution/result.rb +45 -0
  104. data/packages/igniter-app/lib/igniter/app/evolution/runner.rb +102 -0
  105. data/packages/igniter-app/lib/igniter/app/evolution/store.rb +21 -0
  106. data/packages/igniter-app/lib/igniter/app/evolution/stores/file_store.rb +241 -0
  107. data/packages/igniter-app/lib/igniter/app/evolution/trail.rb +108 -0
  108. data/packages/igniter-app/lib/igniter/app/evolution.rb +11 -0
  109. data/packages/igniter-app/lib/igniter/app/filesystem_loader_adapter.rb +21 -0
  110. data/packages/igniter-app/lib/igniter/app/generator.rb +636 -0
  111. data/packages/igniter-app/lib/igniter/app/generators/cluster.rb +1367 -0
  112. data/packages/igniter-app/lib/igniter/app/generators/dashboard.rb +152 -0
  113. data/packages/igniter-app/lib/igniter/app/generators/playground.rb +1227 -0
  114. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/README.md.erb +37 -0
  115. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/app.rb.erb +19 -0
  116. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/contexts/home_context.rb.erb +54 -0
  117. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/frontend/application.js.erb +3 -0
  118. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/spec/dashboard_app_spec.rb.erb +79 -0
  119. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/support/stack_overview.rb.erb +23 -0
  120. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/handlers/home_handler.rb.erb +27 -0
  121. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/views/home_page.arb.erb +44 -0
  122. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/views/home_page.rb.erb +56 -0
  123. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/views/layout.arb.erb +17 -0
  124. data/packages/igniter-app/lib/igniter/app/host_adapter.rb +26 -0
  125. data/packages/igniter-app/lib/igniter/app/host_config.rb +40 -0
  126. data/packages/igniter-app/lib/igniter/app/host_registry.rb +43 -0
  127. data/packages/igniter-app/lib/igniter/app/loader_adapter.rb +15 -0
  128. data/packages/igniter-app/lib/igniter/app/loader_pack.rb +8 -0
  129. data/packages/igniter-app/lib/igniter/app/loader_registry.rb +39 -0
  130. data/packages/igniter-app/lib/igniter/app/observability/operator_action_handler.rb +147 -0
  131. data/packages/igniter-app/lib/igniter/app/observability/operator_console_handler.rb +747 -0
  132. data/packages/igniter-app/lib/igniter/app/observability/operator_overview_handler.rb +350 -0
  133. data/packages/igniter-app/lib/igniter/app/observability.rb +5 -0
  134. data/packages/igniter-app/lib/igniter/app/observability_pack.rb +71 -0
  135. data/packages/igniter-app/lib/igniter/app/operator/dispatcher.rb +40 -0
  136. data/packages/igniter-app/lib/igniter/app/operator/handler_registry.rb +40 -0
  137. data/packages/igniter-app/lib/igniter/app/operator/handler_result.rb +67 -0
  138. data/packages/igniter-app/lib/igniter/app/operator/handlers/base.rb +79 -0
  139. data/packages/igniter-app/lib/igniter/app/operator/handlers/ignite_handler.rb +108 -0
  140. data/packages/igniter-app/lib/igniter/app/operator/handlers/orchestration_handler.rb +33 -0
  141. data/packages/igniter-app/lib/igniter/app/operator/handlers.rb +5 -0
  142. data/packages/igniter-app/lib/igniter/app/operator/lifecycle_contract.rb +55 -0
  143. data/packages/igniter-app/lib/igniter/app/operator/policy.rb +157 -0
  144. data/packages/igniter-app/lib/igniter/app/operator.rb +17 -0
  145. data/packages/igniter-app/lib/igniter/app/orchestration/action_result_builder.rb +65 -0
  146. data/packages/igniter-app/lib/igniter/app/orchestration/followup_request.rb +36 -0
  147. data/packages/igniter-app/lib/igniter/app/orchestration/handler_registry.rb +58 -0
  148. data/packages/igniter-app/lib/igniter/app/orchestration/handlers.rb +106 -0
  149. data/packages/igniter-app/lib/igniter/app/orchestration/inbox.rb +283 -0
  150. data/packages/igniter-app/lib/igniter/app/orchestration/inbox_query.rb +293 -0
  151. data/packages/igniter-app/lib/igniter/app/orchestration/lane_registry.rb +100 -0
  152. data/packages/igniter-app/lib/igniter/app/orchestration/operator_query.rb +449 -0
  153. data/packages/igniter-app/lib/igniter/app/orchestration/plan.rb +68 -0
  154. data/packages/igniter-app/lib/igniter/app/orchestration/planner.rb +89 -0
  155. data/packages/igniter-app/lib/igniter/app/orchestration/policies.rb +125 -0
  156. data/packages/igniter-app/lib/igniter/app/orchestration/policy_registry.rb +63 -0
  157. data/packages/igniter-app/lib/igniter/app/orchestration/result.rb +43 -0
  158. data/packages/igniter-app/lib/igniter/app/orchestration/routing_registry.rb +43 -0
  159. data/packages/igniter-app/lib/igniter/app/orchestration/runner.rb +50 -0
  160. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_event_query.rb +205 -0
  161. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_overview_builder.rb +286 -0
  162. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_query_overview_builder.rb +20 -0
  163. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_result_builder.rb +23 -0
  164. data/packages/igniter-app/lib/igniter/app/orchestration.rb +113 -0
  165. data/packages/igniter-app/lib/igniter/app/runtime.rb +4 -0
  166. data/packages/igniter-app/lib/igniter/app/runtime_context.rb +101 -0
  167. data/packages/igniter-app/lib/igniter/app/runtime_pack.rb +16 -0
  168. data/packages/igniter-app/lib/igniter/app/scaffold_pack.rb +6 -0
  169. data/{lib/igniter/application → packages/igniter-app/lib/igniter/app}/scheduler.rb +2 -2
  170. data/packages/igniter-app/lib/igniter/app/scheduler_adapter.rb +17 -0
  171. data/packages/igniter-app/lib/igniter/app/scheduler_pack.rb +8 -0
  172. data/packages/igniter-app/lib/igniter/app/scheduler_registry.rb +39 -0
  173. data/packages/igniter-app/lib/igniter/app/stack.rb +1726 -0
  174. data/packages/igniter-app/lib/igniter/app/stack_pack.rb +3 -0
  175. data/packages/igniter-app/lib/igniter/app/threaded_scheduler_adapter.rb +35 -0
  176. data/packages/igniter-app/lib/igniter/app/yml_loader.rb +43 -0
  177. data/packages/igniter-app/lib/igniter/app.rb +2367 -0
  178. data/packages/igniter-app/lib/igniter/ignite/bootstrap_agent.rb +334 -0
  179. data/packages/igniter-app/lib/igniter/ignite/bootstrap_target.rb +79 -0
  180. data/packages/igniter-app/lib/igniter/ignite/deployment_intent.rb +82 -0
  181. data/packages/igniter-app/lib/igniter/ignite/ignition_agent.rb +1011 -0
  182. data/packages/igniter-app/lib/igniter/ignite/ignition_plan.rb +83 -0
  183. data/packages/igniter-app/lib/igniter/ignite/ignition_report.rb +144 -0
  184. data/packages/igniter-app/lib/igniter/ignite/store.rb +19 -0
  185. data/packages/igniter-app/lib/igniter/ignite/stores/file_store.rb +112 -0
  186. data/packages/igniter-app/lib/igniter/ignite/trail.rb +215 -0
  187. data/packages/igniter-app/lib/igniter/ignite.rb +11 -0
  188. data/packages/igniter-app/lib/igniter-app.rb +5 -0
  189. data/packages/igniter-cluster/README.md +9 -0
  190. data/packages/igniter-cluster/lib/igniter/cluster/agent_route_resolver.rb +58 -0
  191. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/consensus/cluster.rb +8 -4
  192. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/consensus/errors.rb +3 -1
  193. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/consensus/executors.rb +3 -1
  194. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/consensus/node.rb +3 -1
  195. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/consensus/read_query.rb +5 -3
  196. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/consensus/state_machine.rb +4 -2
  197. data/packages/igniter-cluster/lib/igniter/cluster/consensus.rb +18 -0
  198. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics/governance_contributor.rb +90 -0
  199. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics/identity_contributor.rb +98 -0
  200. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics/routing_contributor.rb +674 -0
  201. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics.rb +24 -0
  202. data/packages/igniter-cluster/lib/igniter/cluster/events/envelope.rb +136 -0
  203. data/packages/igniter-cluster/lib/igniter/cluster/events/hook_support.rb +33 -0
  204. data/packages/igniter-cluster/lib/igniter/cluster/events/log.rb +102 -0
  205. data/packages/igniter-cluster/lib/igniter/cluster/events/projection_feed.rb +98 -0
  206. data/packages/igniter-cluster/lib/igniter/cluster/events/read_model_projector.rb +32 -0
  207. data/packages/igniter-cluster/lib/igniter/cluster/events.rb +131 -0
  208. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_decision.rb +41 -0
  209. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_policy.rb +66 -0
  210. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_queue.rb +88 -0
  211. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_request.rb +62 -0
  212. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_workflow.rb +214 -0
  213. data/packages/igniter-cluster/lib/igniter/cluster/governance/checkpoint.rb +141 -0
  214. data/packages/igniter-cluster/lib/igniter/cluster/governance/compaction_record.rb +33 -0
  215. data/packages/igniter-cluster/lib/igniter/cluster/governance/stores/checkpoint_store.rb +89 -0
  216. data/packages/igniter-cluster/lib/igniter/cluster/governance/stores/file_store.rb +249 -0
  217. data/packages/igniter-cluster/lib/igniter/cluster/governance/trail.rb +164 -0
  218. data/packages/igniter-cluster/lib/igniter/cluster/governance.rb +12 -0
  219. data/packages/igniter-cluster/lib/igniter/cluster/identity/capability_attestation.rb +114 -0
  220. data/packages/igniter-cluster/lib/igniter/cluster/identity/manifest.rb +139 -0
  221. data/packages/igniter-cluster/lib/igniter/cluster/identity/node_identity.rb +106 -0
  222. data/packages/igniter-cluster/lib/igniter/cluster/identity.rb +5 -0
  223. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/mesh/announcer.rb +37 -4
  224. data/packages/igniter-cluster/lib/igniter/cluster/mesh/checkpoint_gossip.rb +60 -0
  225. data/packages/igniter-cluster/lib/igniter/cluster/mesh/config.rb +146 -0
  226. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/mesh/discovery.rb +7 -2
  227. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/mesh/errors.rb +10 -5
  228. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/mesh/gossip.rb +19 -4
  229. data/packages/igniter-cluster/lib/igniter/cluster/mesh/mesh_ql.rb +470 -0
  230. data/packages/igniter-cluster/lib/igniter/cluster/mesh/node_observation.rb +281 -0
  231. data/packages/igniter-cluster/lib/igniter/cluster/mesh/observation_query.rb +284 -0
  232. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer.rb +51 -0
  233. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_capacity_report.rb +42 -0
  234. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_identity_envelope.rb +158 -0
  235. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_metadata.rb +122 -0
  236. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_registry.rb +81 -0
  237. data/packages/igniter-cluster/lib/igniter/cluster/mesh/placement_decision.rb +64 -0
  238. data/packages/igniter-cluster/lib/igniter/cluster/mesh/placement_planner.rb +154 -0
  239. data/packages/igniter-cluster/lib/igniter/cluster/mesh/placement_policy.rb +103 -0
  240. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/mesh/poller.rb +19 -4
  241. data/packages/igniter-cluster/lib/igniter/cluster/mesh/rebalance_plan.rb +66 -0
  242. data/packages/igniter-cluster/lib/igniter/cluster/mesh/rebalance_planner.rb +153 -0
  243. data/packages/igniter-cluster/lib/igniter/cluster/mesh/repair_loop.rb +169 -0
  244. data/packages/igniter-cluster/lib/igniter/cluster/mesh/router.rb +306 -0
  245. data/packages/igniter-cluster/lib/igniter/cluster/mesh/workload_signal.rb +46 -0
  246. data/packages/igniter-cluster/lib/igniter/cluster/mesh/workload_tracker.rb +215 -0
  247. data/packages/igniter-cluster/lib/igniter/cluster/mesh.rb +452 -0
  248. data/packages/igniter-cluster/lib/igniter/cluster/ownership/claim.rb +69 -0
  249. data/packages/igniter-cluster/lib/igniter/cluster/ownership/errors.rb +19 -0
  250. data/packages/igniter-cluster/lib/igniter/cluster/ownership/owner_client.rb +76 -0
  251. data/packages/igniter-cluster/lib/igniter/cluster/ownership/registry.rb +98 -0
  252. data/packages/igniter-cluster/lib/igniter/cluster/ownership/resolver.rb +62 -0
  253. data/packages/igniter-cluster/lib/igniter/cluster/ownership.rb +81 -0
  254. data/packages/igniter-cluster/lib/igniter/cluster/projection_store.rb +62 -0
  255. data/packages/igniter-cluster/lib/igniter/cluster/rag/chunk.rb +49 -0
  256. data/packages/igniter-cluster/lib/igniter/cluster/rag/fanout_retriever.rb +93 -0
  257. data/packages/igniter-cluster/lib/igniter/cluster/rag/knowledge_shard.rb +140 -0
  258. data/packages/igniter-cluster/lib/igniter/cluster/rag/net_http_adapter.rb +85 -0
  259. data/packages/igniter-cluster/lib/igniter/cluster/rag/ranker.rb +46 -0
  260. data/packages/igniter-cluster/lib/igniter/cluster/rag/retrieval_query.rb +30 -0
  261. data/packages/igniter-cluster/lib/igniter/cluster/rag/retrieval_result.rb +77 -0
  262. data/packages/igniter-cluster/lib/igniter/cluster/rag.rb +38 -0
  263. data/packages/igniter-cluster/lib/igniter/cluster/remote_adapter.rb +101 -0
  264. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/bootstrapper.rb +3 -1
  265. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/bootstrappers/gem.rb +11 -4
  266. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/bootstrappers/git.rb +9 -2
  267. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/bootstrappers/tarball.rb +8 -2
  268. data/packages/igniter-cluster/lib/igniter/cluster/replication/capability_query.rb +675 -0
  269. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/expansion_plan.rb +5 -3
  270. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/expansion_planner.rb +41 -29
  271. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/manifest.rb +3 -1
  272. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/network_topology.rb +44 -17
  273. data/packages/igniter-cluster/lib/igniter/cluster/replication/node_profile.rb +134 -0
  274. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/reflective_replication_agent.rb +50 -29
  275. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/replication_agent.rb +4 -2
  276. data/{lib/igniter → packages/igniter-cluster/lib/igniter/cluster}/replication/ssh_session.rb +3 -1
  277. data/packages/igniter-cluster/lib/igniter/cluster/replication.rb +38 -0
  278. data/packages/igniter-cluster/lib/igniter/cluster/routed_agent_adapter.rb +79 -0
  279. data/packages/igniter-cluster/lib/igniter/cluster/routing_plan_executor.rb +427 -0
  280. data/packages/igniter-cluster/lib/igniter/cluster/routing_plan_result.rb +38 -0
  281. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_plan.rb +34 -0
  282. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_planner.rb +76 -0
  283. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_result.rb +34 -0
  284. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_runner.rb +125 -0
  285. data/packages/igniter-cluster/lib/igniter/cluster/trust/trust_assessment.rb +37 -0
  286. data/packages/igniter-cluster/lib/igniter/cluster/trust/trust_store.rb +58 -0
  287. data/packages/igniter-cluster/lib/igniter/cluster/trust/verifier.rb +80 -0
  288. data/packages/igniter-cluster/lib/igniter/cluster/trust.rb +9 -0
  289. data/packages/igniter-cluster/lib/igniter/cluster.rb +71 -0
  290. data/packages/igniter-cluster/lib/igniter-cluster.rb +3 -0
  291. data/packages/igniter-core/README.md +21 -0
  292. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/capabilities.rb +3 -1
  293. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/compiled_graph.rb +40 -2
  294. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validation_pipeline.rb +1 -0
  295. data/packages/igniter-core/lib/igniter/core/compiler/validators/agent_validator.rb +142 -0
  296. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/dependencies_validator.rb +40 -1
  297. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler.rb +1 -0
  298. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/contract.rb +76 -6
  299. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dataflow.rb +7 -7
  300. data/packages/igniter-core/lib/igniter/core/diagnostics/agent_contributor.rb +241 -0
  301. data/packages/igniter-core/lib/igniter/core/diagnostics/capability_contributor.rb +162 -0
  302. data/packages/igniter-core/lib/igniter/core/diagnostics/orchestration_contributor.rb +75 -0
  303. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/report.rb +81 -6
  304. data/packages/igniter-core/lib/igniter/core/diagnostics.rb +58 -0
  305. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dsl/contract_builder.rb +163 -6
  306. data/packages/igniter-core/lib/igniter/core/dto/record.rb +189 -0
  307. data/packages/igniter-core/lib/igniter/core/dto.rb +8 -0
  308. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/effect.rb +4 -0
  309. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/errors.rb +26 -3
  310. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/introspection/graph_formatter.rb +32 -1
  311. data/packages/igniter-core/lib/igniter/core/extensions/introspection/plan_formatter.rb +85 -0
  312. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/introspection/runtime_formatter.rb +26 -0
  313. data/packages/igniter-core/lib/igniter/core/extensions/invariants.rb +70 -0
  314. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental.rb +4 -4
  315. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/memorable.rb +1 -1
  316. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/stores/sqlite.rb +5 -3
  317. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory.rb +11 -11
  318. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/metrics/prometheus_exporter.rb +1 -1
  319. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/metrics.rb +8 -8
  320. data/packages/igniter-core/lib/igniter/core/model/agent_interaction_contract.rb +172 -0
  321. data/packages/igniter-core/lib/igniter/core/model/agent_node.rb +86 -0
  322. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/branch_node.rb +37 -1
  323. data/packages/igniter-core/lib/igniter/core/model/remote_node.rb +91 -0
  324. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model.rb +2 -0
  325. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/node_cache.rb +1 -1
  326. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing.rb +8 -8
  327. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/builder.rb +30 -1
  328. data/packages/igniter-core/lib/igniter/core/runtime/agent_adapter.rb +41 -0
  329. data/packages/igniter-core/lib/igniter/core/runtime/agent_result_contract.rb +91 -0
  330. data/packages/igniter-core/lib/igniter/core/runtime/agent_route.rb +60 -0
  331. data/packages/igniter-core/lib/igniter/core/runtime/agent_route_resolver.rb +26 -0
  332. data/packages/igniter-core/lib/igniter/core/runtime/agent_session.rb +922 -0
  333. data/packages/igniter-core/lib/igniter/core/runtime/agent_session_query.rb +379 -0
  334. data/packages/igniter-core/lib/igniter/core/runtime/agent_transport.rb +30 -0
  335. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/cache.rb +6 -3
  336. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/deferred_result.rb +27 -1
  337. data/packages/igniter-core/lib/igniter/core/runtime/execution.rb +913 -0
  338. data/packages/igniter-core/lib/igniter/core/runtime/job_worker.rb +39 -0
  339. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/node_state.rb +4 -2
  340. data/packages/igniter-core/lib/igniter/core/runtime/orchestration_overview.rb +213 -0
  341. data/packages/igniter-core/lib/igniter/core/runtime/orchestration_runtime_state.rb +176 -0
  342. data/packages/igniter-core/lib/igniter/core/runtime/orchestration_transition_query.rb +208 -0
  343. data/packages/igniter-core/lib/igniter/core/runtime/planner.rb +301 -0
  344. data/packages/igniter-core/lib/igniter/core/runtime/proxy_agent_adapter.rb +124 -0
  345. data/packages/igniter-core/lib/igniter/core/runtime/remote_adapter.rb +26 -0
  346. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/resolver.rb +250 -57
  347. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/result.rb +2 -0
  348. data/packages/igniter-core/lib/igniter/core/runtime/stores/sqlite_store.rb +155 -0
  349. data/packages/igniter-core/lib/igniter/core/runtime/stream_result.rb +171 -0
  350. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime.rb +15 -0
  351. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/temporal.rb +1 -1
  352. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/tool/discoverable.rb +3 -3
  353. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/tool.rb +6 -2
  354. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/version.rb +1 -1
  355. data/packages/igniter-core/lib/igniter/core.rb +23 -0
  356. data/packages/igniter-core/lib/igniter-core.rb +3 -0
  357. data/packages/igniter-extensions/README.md +21 -0
  358. data/packages/igniter-extensions/lib/igniter/extensions/auditing.rb +3 -0
  359. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/capabilities.rb +1 -1
  360. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/content_addressing.rb +1 -1
  361. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/dataflow.rb +1 -1
  362. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/differential.rb +1 -1
  363. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/execution_report.rb +1 -1
  364. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/incremental.rb +1 -1
  365. data/packages/igniter-extensions/lib/igniter/extensions/introspection.rb +3 -0
  366. data/packages/igniter-extensions/lib/igniter/extensions/invariants.rb +3 -0
  367. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/provenance.rb +1 -1
  368. data/packages/igniter-extensions/lib/igniter/extensions/reactive.rb +3 -0
  369. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/saga.rb +1 -1
  370. data/packages/igniter-extensions/lib/igniter/extensions.rb +8 -0
  371. data/packages/igniter-extensions/lib/igniter-extensions.rb +3 -0
  372. data/packages/igniter-frontend/README.md +224 -0
  373. data/packages/igniter-frontend/lib/igniter/frontend/app.rb +90 -0
  374. data/packages/igniter-frontend/lib/igniter/frontend/app_access.rb +36 -0
  375. data/packages/igniter-frontend/lib/igniter/frontend/arbre/component.rb +120 -0
  376. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/action_group.rb +53 -0
  377. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/badge.rb +91 -0
  378. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/boolean.rb +53 -0
  379. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/breadcrumbs.rb +71 -0
  380. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/card.rb +114 -0
  381. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/conversation_panel.rb +61 -0
  382. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/datetime.rb +42 -0
  383. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/display_value_support.rb +38 -0
  384. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/empty_state.rb +39 -0
  385. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/event_list.rb +44 -0
  386. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/filters.rb +183 -0
  387. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/indicator.rb +59 -0
  388. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/json_panel.rb +36 -0
  389. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/key_value_list.rb +40 -0
  390. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/loading_state.rb +43 -0
  391. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/metric_grid.rb +37 -0
  392. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/number.rb +53 -0
  393. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/page_header.rb +53 -0
  394. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/pagination.rb +143 -0
  395. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/panel.rb +67 -0
  396. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/percentage.rb +79 -0
  397. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/resource_list.rb +38 -0
  398. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/scenario_card.rb +48 -0
  399. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/shell_columns.rb +67 -0
  400. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/sidebar_shell.rb +106 -0
  401. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/table_with.rb +203 -0
  402. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/tabs.rb +147 -0
  403. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/viz.rb +185 -0
  404. data/packages/igniter-frontend/lib/igniter/frontend/arbre/page.rb +74 -0
  405. data/packages/igniter-frontend/lib/igniter/frontend/arbre/raw_text_node.rb +40 -0
  406. data/packages/igniter-frontend/lib/igniter/frontend/arbre/template_page.rb +243 -0
  407. data/packages/igniter-frontend/lib/igniter/frontend/arbre.rb +48 -0
  408. data/packages/igniter-frontend/lib/igniter/frontend/arbre_page.rb +7 -0
  409. data/packages/igniter-frontend/lib/igniter/frontend/assets.rb +101 -0
  410. data/packages/igniter-frontend/lib/igniter/frontend/builder.rb +124 -0
  411. data/packages/igniter-frontend/lib/igniter/frontend/component.rb +24 -0
  412. data/packages/igniter-frontend/lib/igniter/frontend/components.rb +7 -0
  413. data/packages/igniter-frontend/lib/igniter/frontend/context.rb +53 -0
  414. data/packages/igniter-frontend/lib/igniter/frontend/form_builder.rb +63 -0
  415. data/packages/igniter-frontend/lib/igniter/frontend/handler.rb +92 -0
  416. data/packages/igniter-frontend/lib/igniter/frontend/javascript.rb +353 -0
  417. data/packages/igniter-frontend/lib/igniter/frontend/page.rb +24 -0
  418. data/packages/igniter-frontend/lib/igniter/frontend/request.rb +61 -0
  419. data/packages/igniter-frontend/lib/igniter/frontend/response.rb +67 -0
  420. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/realtime/adapters.rb +226 -0
  421. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/realtime/presets.rb +147 -0
  422. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/realtime.rb +259 -0
  423. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/surfaces.rb +1074 -0
  424. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/ui.rb +1438 -0
  425. data/packages/igniter-frontend/lib/igniter/frontend/tailwind.rb +180 -0
  426. data/packages/igniter-frontend/lib/igniter/frontend/version.rb +9 -0
  427. data/packages/igniter-frontend/lib/igniter/frontend.rb +35 -0
  428. data/packages/igniter-frontend/lib/igniter-frontend.rb +3 -0
  429. data/packages/igniter-rails/README.md +96 -0
  430. data/packages/igniter-rails/lib/igniter/plugins/rails/generators/contract/templates/contract.rb.tt +22 -0
  431. data/packages/igniter-rails/lib/igniter/plugins/rails/generators/install/templates/igniter.rb.tt +16 -0
  432. data/packages/igniter-rails/lib/igniter-rails.rb +3 -0
  433. data/packages/igniter-schema-rendering/README.md +27 -0
  434. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/page.rb +35 -0
  435. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/patcher.rb +47 -0
  436. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/renderer.rb +268 -0
  437. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/schema.rb +172 -0
  438. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/store.rb +53 -0
  439. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/submission_normalizer.rb +117 -0
  440. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/submission_processor.rb +91 -0
  441. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/submission_validator.rb +62 -0
  442. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/version.rb +9 -0
  443. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering.rb +20 -0
  444. data/packages/igniter-schema-rendering/lib/igniter-schema-rendering.rb +3 -0
  445. data/packages/igniter-sdk/README.md +25 -0
  446. data/packages/igniter-sdk/lib/igniter/sdk/channels/base.rb +84 -0
  447. data/packages/igniter-sdk/lib/igniter/sdk/channels/delivery_result.rb +61 -0
  448. data/packages/igniter-sdk/lib/igniter/sdk/channels/message.rb +101 -0
  449. data/packages/igniter-sdk/lib/igniter/sdk/channels/telegram.rb +161 -0
  450. data/packages/igniter-sdk/lib/igniter/sdk/channels/webhook.rb +213 -0
  451. data/packages/igniter-sdk/lib/igniter/sdk/channels.rb +17 -0
  452. data/packages/igniter-sdk/lib/igniter/sdk/data/store.rb +31 -0
  453. data/packages/igniter-sdk/lib/igniter/sdk/data/stores/file.rb +113 -0
  454. data/packages/igniter-sdk/lib/igniter/sdk/data/stores/in_memory.rb +63 -0
  455. data/packages/igniter-sdk/lib/igniter/sdk/data/stores/sqlite.rb +144 -0
  456. data/packages/igniter-sdk/lib/igniter/sdk/data.rb +34 -0
  457. data/packages/igniter-sdk/lib/igniter/sdk/tools/agent_bootstrap_tool.rb +151 -0
  458. data/packages/igniter-sdk/lib/igniter/sdk/tools/local_workflow_selector_tool.rb +269 -0
  459. data/packages/igniter-sdk/lib/igniter/sdk/tools/system_discovery_tool.rb +198 -0
  460. data/packages/igniter-sdk/lib/igniter/sdk/tools.rb +9 -0
  461. data/packages/igniter-sdk/lib/igniter/sdk.rb +86 -0
  462. data/packages/igniter-sdk/lib/igniter-sdk.rb +3 -0
  463. data/packages/igniter-server/README.md +9 -0
  464. data/packages/igniter-server/lib/igniter/server/agent_session_store.rb +98 -0
  465. data/packages/igniter-server/lib/igniter/server/agent_transport.rb +95 -0
  466. data/packages/igniter-server/lib/igniter/server/app_host.rb +3 -0
  467. data/{lib → packages/igniter-server/lib}/igniter/server/client.rb +96 -6
  468. data/packages/igniter-server/lib/igniter/server/config.rb +70 -0
  469. data/packages/igniter-server/lib/igniter/server/handlers/agent_message_handler.rb +107 -0
  470. data/packages/igniter-server/lib/igniter/server/handlers/agent_session_handler.rb +125 -0
  471. data/packages/igniter-server/lib/igniter/server/handlers/manifest_handler.rb +77 -0
  472. data/{lib → packages/igniter-server/lib}/igniter/server/handlers/metrics_handler.rb +1 -1
  473. data/{lib → packages/igniter-server/lib}/igniter/server/handlers/peers_handler.rb +38 -17
  474. data/{lib → packages/igniter-server/lib}/igniter/server/http_server.rb +91 -15
  475. data/{lib → packages/igniter-server/lib}/igniter/server/rack_app.rb +27 -2
  476. data/packages/igniter-server/lib/igniter/server/remote_adapter.rb +27 -0
  477. data/packages/igniter-server/lib/igniter/server/router.rb +291 -0
  478. data/{lib → packages/igniter-server/lib}/igniter/server/server_logger.rb +3 -1
  479. data/{lib → packages/igniter-server/lib}/igniter/server.rb +58 -1
  480. data/packages/igniter-server/lib/igniter-server.rb +3 -0
  481. metadata +631 -282
  482. data/docs/API_V2.md +0 -537
  483. data/docs/APPLICATION_V1.md +0 -253
  484. data/docs/ARCHITECTURE_V2.md +0 -317
  485. data/docs/BACKLOG.md +0 -166
  486. data/docs/BRANCHES_V1.md +0 -213
  487. data/docs/CAPABILITIES_V1.md +0 -207
  488. data/docs/COLLECTIONS_V1.md +0 -303
  489. data/docs/CONSENSUS_V1.md +0 -477
  490. data/docs/CONTENT_ADDRESSING_V1.md +0 -221
  491. data/docs/DATAFLOW_V1.md +0 -274
  492. data/docs/DISTRIBUTED_CONTRACTS_V1.md +0 -493
  493. data/docs/EXECUTION_MODEL_V2.md +0 -324
  494. data/docs/IGNITER_CONCEPTS.md +0 -81
  495. data/docs/LLM_V1.md +0 -335
  496. data/docs/MESH_V1.md +0 -732
  497. data/docs/NODE_CACHE_V1.md +0 -324
  498. data/docs/PATTERNS.md +0 -411
  499. data/docs/PROACTIVE_AGENTS_V1.md +0 -293
  500. data/docs/SERVER_V1.md +0 -512
  501. data/docs/SKILLS_V1.md +0 -213
  502. data/docs/STORE_ADAPTERS.md +0 -154
  503. data/docs/TEMPORAL_V1.md +0 -174
  504. data/docs/TOOLS_V1.md +0 -347
  505. data/docs/TRANSCRIPTION_V1.md +0 -403
  506. data/lib/igniter/agents.rb +0 -56
  507. data/lib/igniter/application/app_config.rb +0 -32
  508. data/lib/igniter/application/generator.rb +0 -157
  509. data/lib/igniter/application/yml_loader.rb +0 -39
  510. data/lib/igniter/application.rb +0 -174
  511. data/lib/igniter/consensus.rb +0 -58
  512. data/lib/igniter/diagnostics.rb +0 -8
  513. data/lib/igniter/extensions/introspection/plan_formatter.rb +0 -55
  514. data/lib/igniter/extensions/invariants.rb +0 -116
  515. data/lib/igniter/extensions/mesh.rb +0 -31
  516. data/lib/igniter/integrations/agents.rb +0 -18
  517. data/lib/igniter/mesh/config.rb +0 -45
  518. data/lib/igniter/mesh/peer.rb +0 -21
  519. data/lib/igniter/mesh/peer_registry.rb +0 -51
  520. data/lib/igniter/mesh/router.rb +0 -109
  521. data/lib/igniter/mesh.rb +0 -85
  522. data/lib/igniter/model/remote_node.rb +0 -42
  523. data/lib/igniter/replication/node_role.rb +0 -42
  524. data/lib/igniter/replication/role_registry.rb +0 -73
  525. data/lib/igniter/replication.rb +0 -54
  526. data/lib/igniter/runtime/execution.rb +0 -416
  527. data/lib/igniter/runtime/job_worker.rb +0 -18
  528. data/lib/igniter/runtime/planner.rb +0 -126
  529. data/lib/igniter/server/config.rb +0 -34
  530. data/lib/igniter/server/handlers/manifest_handler.rb +0 -34
  531. data/lib/igniter/server/router.rb +0 -108
  532. data/lib/igniter/skill.rb +0 -218
  533. data/lib/igniter/tool_registry.rb +0 -144
  534. /data/{lib → packages/igniter-agents/lib}/igniter/agent/mailbox.rb +0 -0
  535. /data/{lib → packages/igniter-agents/lib}/igniter/agent/message.rb +0 -0
  536. /data/{lib → packages/igniter-agents/lib}/igniter/agent/state_holder.rb +0 -0
  537. /data/{lib → packages/igniter-agents/lib}/igniter/agents/observability/metrics_agent.rb +0 -0
  538. /data/{lib → packages/igniter-agents/lib}/igniter/agents/pipeline/batch_processor_agent.rb +0 -0
  539. /data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/agents/proactive}/alert_agent.rb +0 -0
  540. /data/{lib/igniter/agents/ai → packages/igniter-agents/lib/igniter/agents/proactive}/health_check_agent.rb +0 -0
  541. /data/{lib → packages/igniter-agents/lib}/igniter/agents/reliability/retry_agent.rb +0 -0
  542. /data/{lib → packages/igniter-agents/lib}/igniter/agents/scheduling/cron_agent.rb +0 -0
  543. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/graph_compiler.rb +0 -0
  544. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/type_resolver.rb +0 -0
  545. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validation_context.rb +0 -0
  546. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validator.rb +0 -0
  547. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/await_validator.rb +0 -0
  548. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/callable_validator.rb +0 -0
  549. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/outputs_validator.rb +0 -0
  550. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/remote_validator.rb +0 -0
  551. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/type_compatibility_validator.rb +0 -0
  552. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/uniqueness_validator.rb +0 -0
  553. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/content_addressing.rb +0 -0
  554. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dataflow/aggregate_operators.rb +0 -0
  555. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dataflow/aggregate_state.rb +0 -0
  556. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dataflow/diff.rb +0 -0
  557. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dataflow/diff_state.rb +0 -0
  558. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dataflow/incremental_collection_result.rb +0 -0
  559. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dataflow/window_filter.rb +0 -0
  560. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/auditing/report/console_formatter.rb +0 -0
  561. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/auditing/report/markdown_formatter.rb +0 -0
  562. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/introspection/formatters/mermaid_formatter.rb +0 -0
  563. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/introspection/formatters/text_tree_formatter.rb +0 -0
  564. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/divergence.rb +0 -0
  565. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/formatter.rb +0 -0
  566. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/report.rb +0 -0
  567. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/runner.rb +0 -0
  568. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential.rb +0 -0
  569. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dsl/schema_builder.rb +0 -0
  570. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dsl.rb +0 -0
  571. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/effect_registry.rb +0 -0
  572. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/events/bus.rb +0 -0
  573. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/events/event.rb +0 -0
  574. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/events.rb +0 -0
  575. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/builder.rb +0 -0
  576. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/formatter.rb +0 -0
  577. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/node_entry.rb +0 -0
  578. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/report.rb +0 -0
  579. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report.rb +0 -0
  580. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/executor.rb +0 -0
  581. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/executor_registry.rb +0 -0
  582. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/auditing/timeline.rb +0 -0
  583. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/auditing.rb +0 -0
  584. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/introspection.rb +0 -0
  585. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive/engine.rb +0 -0
  586. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive/matcher.rb +0 -0
  587. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive/reaction.rb +0 -0
  588. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive.rb +0 -0
  589. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions.rb +0 -0
  590. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/fingerprint.rb +0 -0
  591. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental/formatter.rb +0 -0
  592. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental/result.rb +0 -0
  593. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental/tracker.rb +0 -0
  594. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/invariant.rb +0 -0
  595. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/agent_memory.rb +0 -0
  596. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/episode.rb +0 -0
  597. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/fact.rb +0 -0
  598. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/reflection_cycle.rb +0 -0
  599. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/reflection_record.rb +0 -0
  600. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/store.rb +0 -0
  601. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/memory/stores/in_memory.rb +0 -0
  602. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/metrics/collector.rb +0 -0
  603. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/metrics/snapshot.rb +0 -0
  604. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/aggregate_node.rb +0 -0
  605. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/await_node.rb +0 -0
  606. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/collection_node.rb +0 -0
  607. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/composition_node.rb +0 -0
  608. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/compute_node.rb +0 -0
  609. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/effect_node.rb +0 -0
  610. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/graph.rb +0 -0
  611. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/input_node.rb +0 -0
  612. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/node.rb +0 -0
  613. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/output_node.rb +0 -0
  614. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/formatter.rb +0 -0
  615. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/generators.rb +0 -0
  616. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/result.rb +0 -0
  617. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/run.rb +0 -0
  618. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/runner.rb +0 -0
  619. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/lineage.rb +0 -0
  620. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/node_trace.rb +0 -0
  621. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/text_formatter.rb +0 -0
  622. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance.rb +0 -0
  623. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/collection_result.rb +0 -0
  624. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/input_validator.rb +0 -0
  625. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/invalidator.rb +0 -0
  626. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runner_factory.rb +0 -0
  627. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runners/inline_runner.rb +0 -0
  628. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runners/store_runner.rb +0 -0
  629. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runners/thread_pool_runner.rb +0 -0
  630. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/stores/active_record_store.rb +0 -0
  631. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/stores/file_store.rb +0 -0
  632. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/stores/memory_store.rb +0 -0
  633. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/stores/redis_store.rb +0 -0
  634. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/compensation.rb +0 -0
  635. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/compensation_record.rb +0 -0
  636. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/executor.rb +0 -0
  637. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/formatter.rb +0 -0
  638. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/result.rb +0 -0
  639. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga.rb +0 -0
  640. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/stream_loop.rb +0 -0
  641. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/type_system.rb +0 -0
  642. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/cable_adapter.rb +0 -0
  643. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/contract_job.rb +0 -0
  644. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/generators/contract/contract_generator.rb +0 -0
  645. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/generators/install/install_generator.rb +0 -0
  646. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/railtie.rb +0 -0
  647. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/webhook_concern.rb +0 -0
  648. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails.rb +0 -0
  649. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/base.rb +0 -0
  650. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/contracts_handler.rb +0 -0
  651. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/event_handler.rb +0 -0
  652. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/execute_handler.rb +0 -0
  653. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/health_handler.rb +0 -0
  654. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/liveness_handler.rb +0 -0
  655. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/readiness_handler.rb +0 -0
  656. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/status_handler.rb +0 -0
  657. /data/{lib → packages/igniter-server/lib}/igniter/server/registry.rb +0 -0
@@ -0,0 +1,1726 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "irb"
5
+ require "optparse"
6
+ require "securerandom"
7
+ require "shellwords"
8
+ require "stringio"
9
+ require "time"
10
+ require "yaml"
11
+ require "igniter/ignite"
12
+
13
+ module Igniter
14
+ # Root coordinator for mounted multi-app stacks.
15
+ #
16
+ # A stack owns:
17
+ # - shared load paths for repo-level support code
18
+ # - a registry of named apps under apps/<name>
19
+ # - a single mounted runtime rooted in stack.rb + stack.yml
20
+ # - optional local node profiles for multi-instance boot
21
+ #
22
+ # Apps are pluggable mounted packages. Nodes are launch profiles of the same
23
+ # stack. The stack itself owns the server/runtime boundary.
24
+ class Stack
25
+ UNDEFINED_IGNITION_STORE = Object.new
26
+ UNDEFINED_CREDENTIAL_STORE = Object.new
27
+ AppDefinition = Struct.new(:name, :path, :klass, :root, :access_to, keyword_init: true)
28
+ ConsoleContext = Struct.new(
29
+ :stack_class,
30
+ :root_app_name,
31
+ :root_app_class,
32
+ :app_name,
33
+ :app_class,
34
+ :node_name,
35
+ :node_profile,
36
+ :deployment,
37
+ :runtime,
38
+ :mounts,
39
+ :stack_settings,
40
+ :mesh,
41
+ keyword_init: true
42
+ )
43
+ SERVICE_MOUNT_METHODS = %w[GET POST PUT PATCH DELETE OPTIONS HEAD].freeze
44
+
45
+ class << self
46
+ def root_dir(path = nil)
47
+ return @root_dir unless path
48
+
49
+ @root_dir = File.expand_path(path)
50
+ reset_stack_state!
51
+ end
52
+
53
+ def shared_lib_path(path)
54
+ @shared_lib_paths << path
55
+ end
56
+
57
+ def stack_file(path = nil)
58
+ return @stack_yml_path unless path
59
+
60
+ @stack_yml_path = path
61
+ reset_stack_state!
62
+ end
63
+
64
+ def config_file(path = nil)
65
+ stack_file(path)
66
+ end
67
+
68
+ def environment(name = nil)
69
+ return resolved_environment unless name
70
+
71
+ @environment_name = name.to_s
72
+ reset_stack_state!
73
+ end
74
+
75
+ def environment_file(path = nil)
76
+ return @environment_yml_path unless path
77
+
78
+ @environment_yml_path = path
79
+ reset_stack_state!
80
+ end
81
+
82
+ def compose_file(path = nil)
83
+ return @compose_yml_path unless path
84
+
85
+ @compose_yml_path = path
86
+ end
87
+
88
+ def ignition_store(store = UNDEFINED_IGNITION_STORE)
89
+ return (@ignition_store ||= default_ignition_store) if store.equal?(UNDEFINED_IGNITION_STORE)
90
+
91
+ @ignition_store = store
92
+ reload_ignition_trail!
93
+ end
94
+
95
+ def ignition_log(path, retain_events: nil, archive: nil)
96
+ ignition_store(
97
+ Igniter::Ignite::Stores::FileStore.new(
98
+ path: resolve_path(path),
99
+ max_events: retain_events,
100
+ archive_path: archive ? resolve_path(archive) : nil
101
+ )
102
+ )
103
+ end
104
+
105
+ def credential_store(store = UNDEFINED_CREDENTIAL_STORE)
106
+ return (@credential_store ||= default_credential_store) if store.equal?(UNDEFINED_CREDENTIAL_STORE)
107
+
108
+ @credential_store = store
109
+ reload_credential_trail!
110
+ end
111
+
112
+ def credential_log(path, retain_events: nil, archive: nil)
113
+ credential_store(
114
+ Igniter::App::Credentials::Stores::FileStore.new(
115
+ path: resolve_path(path),
116
+ max_events: retain_events,
117
+ archive_path: archive ? resolve_path(archive) : nil
118
+ )
119
+ )
120
+ end
121
+
122
+ def procfile_dev_file(path = nil)
123
+ return @procfile_dev_path unless path
124
+
125
+ @procfile_dev_path = path
126
+ end
127
+
128
+ def app(name = nil, path: nil, klass: nil, default: false, access_to: [])
129
+ if path || klass
130
+ raise ArgumentError, "stack app registration requires both path: and klass:" unless path && klass
131
+
132
+ definition = AppDefinition.new(
133
+ name: name.to_sym,
134
+ path: path.to_s,
135
+ klass: klass,
136
+ root: default,
137
+ access_to: Array(access_to).map(&:to_sym)
138
+ )
139
+
140
+ @apps[definition.name] = definition
141
+ definition.klass.bind_stack_context(
142
+ stack_class: self,
143
+ app_name: definition.name,
144
+ access_to: definition.access_to
145
+ ) if definition.klass.respond_to?(:bind_stack_context)
146
+ @root_app = definition.name if default
147
+ return definition.klass
148
+ end
149
+
150
+ app_class(name)
151
+ end
152
+
153
+ def mount(name, at:)
154
+ app_definition(name)
155
+ @mounts[normalize_app_name(name)] = normalize_mount_path(at)
156
+ end
157
+
158
+ def mounts
159
+ @mounts.each_with_object({}) do |(name, path), result|
160
+ result[name] = path.dup
161
+ end
162
+ end
163
+
164
+ def app_names
165
+ @apps.keys
166
+ end
167
+
168
+ def interface(name)
169
+ @apps.each_value do |definition|
170
+ callable = definition.klass.exposed_interfaces[name.to_sym]
171
+ return callable if callable
172
+ end
173
+ raise KeyError, "No registered app exposes interface #{name.inspect}"
174
+ end
175
+
176
+ def interfaces
177
+ @apps.each_value.each_with_object({}) do |definition, hash|
178
+ definition.klass.exposed_interfaces.each { |iface_name, callable| hash[iface_name] = callable }
179
+ end
180
+ end
181
+
182
+ def root_app
183
+ normalize_optional_name(stack_settings.dig("stack", "root_app")) ||
184
+ @root_app ||
185
+ @apps.keys.first
186
+ end
187
+
188
+ def default_node
189
+ return root_app unless nodes_defined?
190
+
191
+ configured = normalize_optional_name(stack_settings.dig("stack", "default_node"))
192
+ return configured if configured && node_names.include?(configured)
193
+
194
+ fallback = normalize_optional_name(ENV["IGNITER_NODE"])
195
+ return fallback if fallback && node_names.include?(fallback)
196
+
197
+ node_names.first
198
+ end
199
+
200
+ def app_definition(name = nil)
201
+ requested = normalize_app_name(name || root_app)
202
+ @apps.fetch(requested) do
203
+ available = @apps.keys.map(&:inspect).join(", ")
204
+ raise ArgumentError, "Unknown stack app #{requested.inspect} (available: #{available})"
205
+ end
206
+ end
207
+
208
+ def app_class(name = nil)
209
+ setup_load_paths!
210
+ app_definition(resolve_app_name(name)).klass
211
+ end
212
+
213
+ def start(name = nil, environment: nil)
214
+ self.environment(environment) if environment
215
+ return start_node(default_node) if use_stack_runtime_by_default?(name)
216
+
217
+ app_class(resolve_app_name(name)).start
218
+ end
219
+
220
+ def rack_app(name = nil, environment: nil)
221
+ self.environment(environment) if environment
222
+ return rack_node(default_node) if use_stack_runtime_by_default?(name)
223
+
224
+ app_class(resolve_app_name(name)).rack_app
225
+ end
226
+
227
+ def node_names
228
+ stack_settings.fetch("nodes", {}).keys.map { |node_name| normalize_app_name(node_name) }
229
+ end
230
+
231
+ def node_profile(name = nil)
232
+ node_name = normalize_app_name(name || default_node)
233
+ stack_settings.fetch("nodes", {}).fetch(node_name.to_s) do
234
+ available = node_names.map(&:inspect).join(", ")
235
+ raise ArgumentError, "Unknown stack node #{node_name.inspect} (available: #{available})"
236
+ end
237
+ end
238
+
239
+ def start_node(name = nil, environment: nil)
240
+ self.environment(environment) if environment
241
+
242
+ runtime = build_stack_runtime(nodes_defined? ? resolve_node_name(name) : nil)
243
+ schedulers = start_stack_schedulers(runtime)
244
+ at_exit { stop_stack_schedulers(schedulers) }
245
+ runtime.fetch(:root_app_class).host_adapter.start(config: runtime.fetch(:root_config))
246
+ end
247
+
248
+ def rack_node(name = nil, environment: nil)
249
+ self.environment(environment) if environment
250
+
251
+ runtime = build_stack_runtime(nodes_defined? ? resolve_node_name(name) : nil)
252
+ start_stack_schedulers(runtime)
253
+ runtime.fetch(:root_app_class).host_adapter.rack_app(config: runtime.fetch(:root_config))
254
+ end
255
+
256
+ def console_context(name = nil, node: nil, environment: nil)
257
+ self.environment(environment) if environment
258
+
259
+ selected_app_name = resolve_app_name(name)
260
+ selected_node_name = nodes_defined? ? resolve_node_name(node) : nil
261
+ selected_runtime = build_stack_runtime(selected_node_name)
262
+
263
+ ConsoleContext.new(
264
+ stack_class: self,
265
+ root_app_name: root_app,
266
+ root_app_class: app_class(root_app),
267
+ app_name: selected_app_name,
268
+ app_class: app_class(selected_app_name),
269
+ node_name: selected_node_name,
270
+ node_profile: selected_node_name ? node_profile(selected_node_name) : nil,
271
+ deployment: deployment_snapshot,
272
+ runtime: selected_runtime,
273
+ mounts: mounts,
274
+ stack_settings: stack_settings,
275
+ mesh: defined?(Igniter::Cluster::Mesh) ? Igniter::Cluster::Mesh : nil
276
+ )
277
+ end
278
+
279
+ def console_binding(name = nil, node: nil, environment: nil)
280
+ context = console_context(name, node: node, environment: environment)
281
+ console_locals_binding(context)
282
+ end
283
+
284
+ def start_console(name = nil, node: nil, environment: nil, output: $stdout, evaluate: nil)
285
+ context = console_context(name, node: node, environment: environment)
286
+ output.puts(console_banner(context))
287
+ bind = console_locals_binding(context)
288
+ return evaluate_console(bind, evaluate, output) if evaluate
289
+
290
+ bind.irb
291
+ end
292
+
293
+ def start_cli(argv = ARGV)
294
+ options = parse_cli_options(argv.dup)
295
+ target = options.delete(:target)
296
+
297
+ if options[:print_procfile_dev]
298
+ self.environment(options[:environment]) if options[:environment]
299
+ puts procfile_dev
300
+ return
301
+ end
302
+
303
+ if options[:write_procfile_dev]
304
+ self.environment(options[:environment]) if options[:environment]
305
+ write_procfile_dev(options[:write_procfile_dev] == true ? nil : options[:write_procfile_dev])
306
+ return
307
+ end
308
+
309
+ if options[:dev]
310
+ start_dev(environment: options[:environment])
311
+ return
312
+ end
313
+
314
+ if options[:print_compose]
315
+ self.environment(options[:environment]) if options[:environment]
316
+ puts compose_yaml
317
+ return
318
+ end
319
+
320
+ if options[:write_compose]
321
+ self.environment(options[:environment]) if options[:environment]
322
+ write_compose(options[:write_compose] == true ? nil : options[:write_compose])
323
+ return
324
+ end
325
+
326
+ if options[:console]
327
+ start_console(
328
+ target,
329
+ node: options[:node],
330
+ environment: options[:environment],
331
+ evaluate: options[:evaluate]
332
+ )
333
+ return
334
+ end
335
+
336
+ if options[:node]
337
+ start_node(options[:node], environment: options[:environment])
338
+ else
339
+ start(target, environment: options[:environment])
340
+ end
341
+ end
342
+
343
+ def stack_settings(reload: false)
344
+ @stack_settings = nil if reload
345
+ @ignition_plan = nil if reload
346
+ @stack_settings ||= deep_merge(
347
+ load_yaml(resolve_path(@stack_yml_path)),
348
+ environment_settings
349
+ )
350
+ end
351
+
352
+ def ignite_settings
353
+ stack_settings.fetch("ignite", {})
354
+ end
355
+
356
+ def ignition_plan(reload: false)
357
+ @ignition_plan = nil if reload
358
+ @ignition_plan ||= build_ignition_plan
359
+ end
360
+
361
+ def ignite(plan: ignition_plan, approved: false, timeout: 5, mesh: nil, request_admission: false, approve_pending_admission: false, bootstrap_remote: false, bootstrap_timeout: 30, session_factory: nil, bootstrapper_factory: nil, await_join: nil, join_timeout: 5, join_poll_interval: 0.1, persist: true)
362
+ agent = Igniter::Ignite::IgnitionAgent.start
363
+ report = agent.call(
364
+ :execute,
365
+ {
366
+ plan: plan,
367
+ runtime_units: runtime_units_snapshot,
368
+ approved: approved,
369
+ mesh: mesh,
370
+ request_admission: request_admission,
371
+ approve_pending_admission: approve_pending_admission,
372
+ bootstrap_remote: bootstrap_remote,
373
+ bootstrap_timeout: bootstrap_timeout,
374
+ session_factory: session_factory,
375
+ bootstrapper_factory: bootstrapper_factory,
376
+ root_dir: @root_dir
377
+ },
378
+ timeout: timeout
379
+ )
380
+ unless should_await_ignite_join?(report, mesh: mesh, await_join: await_join, bootstrap_remote: bootstrap_remote)
381
+ persist_ignition_report!(report, source: :ignite) if persist
382
+ return report
383
+ end
384
+
385
+ report = await_ignite_join(
386
+ report: report,
387
+ mesh: mesh,
388
+ timeout: join_timeout,
389
+ poll_interval: join_poll_interval
390
+ )
391
+ persist_ignition_report!(report, source: :ignite) if persist
392
+ report
393
+ ensure
394
+ agent&.stop(timeout: 1)
395
+ end
396
+
397
+ def ignition_report(**options)
398
+ ignite(**options, persist: false)
399
+ end
400
+
401
+ def confirm_ignite_join(report:, target_id:, url:, mesh: nil, metadata: {}, timeout: 5, persist: true)
402
+ agent = Igniter::Ignite::IgnitionAgent.start
403
+ updated = agent.call(
404
+ :confirm_join,
405
+ {
406
+ report: report,
407
+ target_id: target_id,
408
+ url: url,
409
+ mesh: mesh,
410
+ metadata: metadata
411
+ },
412
+ timeout: timeout
413
+ )
414
+ persist_ignition_report!(updated, source: :confirm_join) if persist
415
+ updated
416
+ ensure
417
+ agent&.stop(timeout: 1)
418
+ end
419
+
420
+ def reconcile_ignite(report:, mesh:, timeout: 5, persist: true)
421
+ agent = Igniter::Ignite::IgnitionAgent.start
422
+ updated = agent.call(
423
+ :reconcile,
424
+ {
425
+ report: report,
426
+ mesh: mesh
427
+ },
428
+ timeout: timeout
429
+ )
430
+ persist_ignition_report!(updated, source: :reconcile) if persist
431
+ updated
432
+ ensure
433
+ agent&.stop(timeout: 1)
434
+ end
435
+
436
+ def detach_ignite_target(report:, target_id:, mesh: nil, metadata: {}, timeout: 5, persist: true, session_factory: nil, decommission_timeout: 30)
437
+ agent = Igniter::Ignite::IgnitionAgent.start
438
+ updated = agent.call(
439
+ :detach,
440
+ {
441
+ report: report,
442
+ target_id: target_id,
443
+ mesh: mesh,
444
+ metadata: metadata,
445
+ root_dir: @root_dir,
446
+ session_factory: session_factory,
447
+ decommission_timeout: decommission_timeout
448
+ },
449
+ timeout: timeout
450
+ )
451
+ persist_ignition_report!(updated, source: :detach) if persist
452
+ updated
453
+ ensure
454
+ agent&.stop(timeout: 1)
455
+ end
456
+
457
+ def teardown_ignite_target(report:, target_id:, mesh: nil, metadata: {}, timeout: 5, persist: true, session_factory: nil, decommission_timeout: 30)
458
+ agent = Igniter::Ignite::IgnitionAgent.start
459
+ updated = agent.call(
460
+ :teardown,
461
+ {
462
+ report: report,
463
+ target_id: target_id,
464
+ mesh: mesh,
465
+ metadata: metadata,
466
+ root_dir: @root_dir,
467
+ session_factory: session_factory,
468
+ decommission_timeout: decommission_timeout
469
+ },
470
+ timeout: timeout
471
+ )
472
+ persist_ignition_report!(updated, source: :teardown) if persist
473
+ updated
474
+ ensure
475
+ agent&.stop(timeout: 1)
476
+ end
477
+
478
+ def reignite_target(target_id:, timeout: 5, persist: true, **options)
479
+ plan = ignition_plan_for_target(target_id, mode: :expand)
480
+ ignite(plan: plan, timeout: timeout, persist: persist, **options)
481
+ end
482
+
483
+ def ignition_trail
484
+ @ignition_trail ||= Igniter::Ignite::Trail.new(store: ignition_store)
485
+ end
486
+
487
+ def ignition_history(limit: 10)
488
+ ignition_trail.snapshot(limit: limit)
489
+ end
490
+
491
+ def latest_ignition_report
492
+ ignition_trail.latest_report
493
+ end
494
+
495
+ def credential_trail
496
+ @credential_trail ||= Igniter::App::Credentials::Trail.new(store: credential_store)
497
+ end
498
+
499
+ def credential_history(limit: 10, filters: nil, order_by: nil, direction: :asc)
500
+ credential_trail.snapshot(limit: limit, filters: filters, order_by: order_by, direction: direction)
501
+ end
502
+
503
+ def credential_request_history(limit: 10, filters: nil, order_by: nil, direction: :asc)
504
+ credential_trail.lease_request_snapshot(
505
+ limit: limit,
506
+ filters: filters,
507
+ order_by: order_by,
508
+ direction: direction
509
+ )
510
+ end
511
+
512
+ def build_credential_lease_request(credential:, target_node:, request_id: nil, requested_scope: :remote, node: nil,
513
+ actor: nil, origin: nil, source:, reason: nil,
514
+ lease_id: nil, requested_at: Time.now.utc.iso8601, metadata: {})
515
+ Igniter::App::Credentials::LeaseRequest.new(
516
+ credential: credential,
517
+ request_id: request_id || SecureRandom.uuid,
518
+ requested_scope: requested_scope,
519
+ node: node,
520
+ target_node: target_node,
521
+ actor: actor,
522
+ origin: origin,
523
+ source: source,
524
+ reason: reason,
525
+ lease_id: lease_id,
526
+ requested_at: requested_at,
527
+ metadata: metadata
528
+ )
529
+ end
530
+
531
+ def request_credential_lease(credential:, target_node:, request_id: nil, requested_scope: :remote, node: nil,
532
+ actor: nil, origin: nil, source:, reason: nil, lease_id: nil,
533
+ requested_at: Time.now.utc.iso8601, metadata: {})
534
+ request = build_credential_lease_request(
535
+ credential: credential,
536
+ request_id: request_id,
537
+ requested_scope: requested_scope,
538
+ node: node,
539
+ target_node: target_node,
540
+ actor: actor,
541
+ origin: origin,
542
+ source: source,
543
+ reason: reason,
544
+ lease_id: lease_id,
545
+ requested_at: requested_at,
546
+ metadata: metadata
547
+ )
548
+
549
+ {
550
+ request: request.to_h,
551
+ policy_allowed: request.policy_allows_request?,
552
+ next_operation: request.policy_allows_request? ? :issue_or_deny : :deny,
553
+ event: record_credential_event(request.request_event)
554
+ }.freeze
555
+ end
556
+
557
+ def issue_credential_lease(request, lease_id: SecureRandom.uuid, actor: nil, origin: nil, source: nil, metadata: {}, timestamp: Time.now.utc.iso8601)
558
+ canonical_request = normalize_credential_lease_request(request).with(lease_id: lease_id)
559
+
560
+ {
561
+ request: canonical_request.to_h,
562
+ policy_allowed: canonical_request.policy_allows_request?,
563
+ event: record_credential_event(
564
+ canonical_request.issue_event(
565
+ lease_id: lease_id,
566
+ actor: actor,
567
+ origin: origin,
568
+ source: source,
569
+ metadata: metadata,
570
+ timestamp: timestamp
571
+ )
572
+ )
573
+ }.freeze
574
+ end
575
+
576
+ def deny_credential_lease(request, reason:, actor: nil, origin: nil, source: nil, metadata: {}, timestamp: Time.now.utc.iso8601)
577
+ canonical_request = normalize_credential_lease_request(request).with(reason: reason)
578
+
579
+ {
580
+ request: canonical_request.to_h,
581
+ policy_allowed: canonical_request.policy_allows_request?,
582
+ event: record_credential_event(
583
+ canonical_request.deny_event(
584
+ reason: reason,
585
+ actor: actor,
586
+ origin: origin,
587
+ source: source,
588
+ metadata: metadata,
589
+ timestamp: timestamp
590
+ )
591
+ )
592
+ }.freeze
593
+ end
594
+
595
+ def revoke_credential_lease(request, lease_id: nil, reason: nil, actor: nil, origin: nil, source: nil, metadata: {}, timestamp: Time.now.utc.iso8601)
596
+ canonical_request = normalize_credential_lease_request(request)
597
+ resolved_lease_id = lease_id || canonical_request.lease_id
598
+
599
+ {
600
+ request: canonical_request.with(lease_id: resolved_lease_id, reason: reason || canonical_request.reason).to_h,
601
+ policy_allowed: canonical_request.policy_allows_request?,
602
+ event: record_credential_event(
603
+ canonical_request.revoke_event(
604
+ lease_id: resolved_lease_id,
605
+ reason: reason,
606
+ actor: actor,
607
+ origin: origin,
608
+ source: source,
609
+ metadata: metadata,
610
+ timestamp: timestamp
611
+ )
612
+ )
613
+ }.freeze
614
+ end
615
+
616
+ def record_credential_event(event = nil, **attributes)
617
+ credential_trail.record(event, **attributes)
618
+ end
619
+
620
+ def reset_credential_trail!
621
+ credential_trail.clear!
622
+ end
623
+
624
+ def reload_credential_trail!
625
+ @credential_trail = Igniter::App::Credentials::Trail.new(store: credential_store)
626
+ end
627
+
628
+ def reset_ignition_trail!
629
+ ignition_trail.clear!
630
+ end
631
+
632
+ def reload_ignition_trail!
633
+ @ignition_trail = Igniter::Ignite::Trail.new(store: ignition_store)
634
+ end
635
+
636
+ def deployment_snapshot
637
+ {
638
+ "stack" => {
639
+ "root_dir" => @root_dir,
640
+ "environment" => resolved_environment,
641
+ "root_app" => root_app.to_s,
642
+ "default_node" => default_node&.to_s,
643
+ "mounts" => stringify_hash(mounts)
644
+ },
645
+ "apps" => app_names.each_with_object({}) do |app_name, result|
646
+ definition = app_definition(app_name)
647
+ app_config = app_deployment(app_name)
648
+ result[app_name.to_s] = app_config.merge(
649
+ "app" => app_name.to_s,
650
+ "path" => definition.path,
651
+ "class_name" => definition.klass.name || definition.klass.inspect,
652
+ "root" => (app_name == root_app)
653
+ )
654
+ end,
655
+ "nodes" => runtime_units_snapshot,
656
+ "ignite" => stringify_nested_keys(ignition_plan.to_h)
657
+ }
658
+ end
659
+
660
+ def compose_config
661
+ compose = stack_settings.dig("deploy", "compose") || {}
662
+ volume_name = compose["volume_name"] || "#{stack_slug}_var"
663
+ working_dir = compose["working_dir"]
664
+ build = compose["build"]
665
+ dockerfile = compose["dockerfile"]
666
+ build_context = compose["context"]
667
+ shared_env = stringify_hash(compose["environment"] || {})
668
+ volume_target = compose["volume_target"]
669
+
670
+ services = runtime_units_snapshot.each_with_object({}) do |(unit_name, unit_config), result|
671
+ service = {}
672
+ service["build"] = build_config(build_context, dockerfile) if build_context || dockerfile || build
673
+ service["image"] = build if build.is_a?(String)
674
+ service["command"] = unit_config["command"] || default_runtime_command(unit_name)
675
+ service["working_dir"] = working_dir if present?(working_dir)
676
+
677
+ env = shared_env.merge(runtime_environment_for_unit(unit_name, unit_config))
678
+ service["environment"] = env unless env.empty?
679
+
680
+ port = unit_config["port"]
681
+ service["ports"] = ["#{port}:#{port}"] if unit_config["public"] && port
682
+
683
+ depends_on = Array(unit_config["depends_on"]).map(&:to_s)
684
+ service["depends_on"] = depends_on unless depends_on.empty?
685
+
686
+ service["volumes"] = ["#{volume_name}:#{volume_target}"] if volume_target
687
+ result[unit_name] = service
688
+ end
689
+
690
+ config = { "services" => services }
691
+ config["volumes"] = { volume_name => {} } if volume_target
692
+ config
693
+ end
694
+
695
+ def compose_yaml
696
+ YAML.dump(compose_config)
697
+ end
698
+
699
+ def write_compose(path = nil)
700
+ target = resolve_path(path || @compose_yml_path)
701
+ FileUtils.mkdir_p(File.dirname(target))
702
+ File.write(target, compose_yaml)
703
+ target
704
+ end
705
+
706
+ def dev_services
707
+ runtime_units_snapshot.map do |unit_name, unit_config|
708
+ {
709
+ name: unit_name.to_s,
710
+ command: unit_config["dev_command"] || unit_config["command"] || default_runtime_command(unit_name),
711
+ environment: dev_runtime_environment_for_unit(unit_name, unit_config)
712
+ }
713
+ end
714
+ end
715
+
716
+ def procfile_dev
717
+ dev_services.map do |service|
718
+ "#{service.fetch(:name)}: #{shell_command_with_env(service.fetch(:command), service.fetch(:environment))}"
719
+ end.join("\n") + "\n"
720
+ end
721
+
722
+ def write_procfile_dev(path = nil)
723
+ target = resolve_path(path || @procfile_dev_path)
724
+ FileUtils.mkdir_p(File.dirname(target))
725
+ File.write(target, procfile_dev)
726
+ target
727
+ end
728
+
729
+ def start_dev(environment: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
730
+ self.environment(environment) if environment
731
+
732
+ services = dev_services
733
+ raise ArgumentError, "No apps registered for stack dev mode" if services.empty?
734
+
735
+ log_dir = resolve_dev_log_dir
736
+ FileUtils.mkdir_p(log_dir)
737
+ $stdout.puts("[stack:dev] writing logs to #{relative_to_root(log_dir)}")
738
+
739
+ processes = {}
740
+ readers = []
741
+ stopping = false
742
+ exit_status = 0
743
+
744
+ stop_all = lambda do |signal|
745
+ next if stopping
746
+
747
+ stopping = true
748
+ processes.each_value do |process|
749
+ begin
750
+ Process.kill(signal, process.fetch(:pid))
751
+ rescue Errno::ESRCH
752
+ nil
753
+ end
754
+ end
755
+ end
756
+
757
+ previous_int = trap("INT") { stop_all.call("TERM") }
758
+ previous_term = trap("TERM") { stop_all.call("TERM") }
759
+
760
+ services.each do |service|
761
+ reader, writer = IO.pipe
762
+ log_path = dev_log_path_for(service.fetch(:name), dir: log_dir)
763
+ pid = Process.spawn(
764
+ service.fetch(:environment),
765
+ service.fetch(:command),
766
+ chdir: @root_dir,
767
+ out: writer,
768
+ err: writer
769
+ )
770
+ writer.close
771
+
772
+ processes[pid] = { name: service.fetch(:name), pid: pid, log_path: log_path }
773
+ readers << Thread.new(reader, service.fetch(:name), log_path) do |io, name, path|
774
+ FileUtils.mkdir_p(File.dirname(path))
775
+ File.open(path, "w") do |log|
776
+ log.sync = true
777
+ log.puts("# igniter dev log")
778
+ log.puts("# service=#{name}")
779
+ log.puts("# started_at=#{Time.now.utc.iso8601}")
780
+ log.puts
781
+
782
+ io.each_line do |line|
783
+ prefixed = "[#{name}] #{line}"
784
+ $stdout.print(prefixed)
785
+ log.print(prefixed)
786
+ end
787
+ end
788
+ ensure
789
+ io.close unless io.closed?
790
+ end
791
+ end
792
+
793
+ until processes.empty?
794
+ pid, status = Process.wait2
795
+ process = processes.delete(pid)
796
+ next unless process
797
+
798
+ exit_status = status.exitstatus if status.exitstatus.to_i != 0 && exit_status.zero?
799
+
800
+ unless stopping
801
+ warn "[stack:dev] #{process.fetch(:name)} exited with status #{status.exitstatus || "unknown"}"
802
+ stop_all.call("TERM")
803
+ end
804
+ end
805
+ ensure
806
+ processes&.each_value do |process|
807
+ begin
808
+ Process.kill("KILL", process.fetch(:pid))
809
+ rescue Errno::ESRCH
810
+ nil
811
+ end
812
+ end
813
+ readers&.each(&:join)
814
+ trap("INT", previous_int) if previous_int
815
+ trap("TERM", previous_term) if previous_term
816
+ raise SystemExit, exit_status unless exit_status.to_i.zero?
817
+ end
818
+
819
+ def setup_load_paths!
820
+ configured_paths = Array(stack_settings.dig("stack", "shared_lib_paths"))
821
+ (@shared_lib_paths + configured_paths).uniq.each do |path|
822
+ full = File.expand_path(path, @root_dir || Dir.pwd)
823
+ $LOAD_PATH.unshift(full) unless $LOAD_PATH.include?(full)
824
+ end
825
+ end
826
+
827
+ def inherited(subclass)
828
+ super
829
+ subclass.instance_variable_set(:@root_dir, Dir.pwd)
830
+ subclass.instance_variable_set(:@stack_yml_path, "stack.yml")
831
+ subclass.instance_variable_set(:@environment_name, nil)
832
+ subclass.instance_variable_set(:@environment_yml_path, nil)
833
+ subclass.instance_variable_set(:@compose_yml_path, "config/deploy/compose.yml")
834
+ subclass.instance_variable_set(:@procfile_dev_path, "config/deploy/Procfile.dev")
835
+ subclass.instance_variable_set(:@shared_lib_paths, [])
836
+ subclass.instance_variable_set(:@apps, {})
837
+ subclass.instance_variable_set(:@mounts, {})
838
+ subclass.instance_variable_set(:@root_app, nil)
839
+ subclass.instance_variable_set(:@stack_settings, nil)
840
+ subclass.instance_variable_set(:@environment_settings, nil)
841
+ subclass.instance_variable_set(:@ignition_store, nil)
842
+ subclass.instance_variable_set(:@ignition_trail, nil)
843
+ subclass.instance_variable_set(:@credential_store, nil)
844
+ subclass.instance_variable_set(:@credential_trail, nil)
845
+ end
846
+
847
+ private
848
+
849
+ def normalize_credential_lease_request(value)
850
+ case value
851
+ when Igniter::App::Credentials::LeaseRequest
852
+ value
853
+ when Hash
854
+ Igniter::App::Credentials::LeaseRequest.from_h(value)
855
+ else
856
+ raise ArgumentError, "request must be a LeaseRequest or Hash"
857
+ end
858
+ end
859
+
860
+ def validate_interface_access!
861
+ exposed = interfaces
862
+ @apps.each_value do |definition|
863
+ definition.access_to.each do |required|
864
+ next if exposed.key?(required)
865
+
866
+ raise ArgumentError,
867
+ "App #{definition.name.inspect} declares access_to #{required.inspect} " \
868
+ "but no registered app exposes it. Known interfaces: #{exposed.keys.inspect}"
869
+ end
870
+ end
871
+ end
872
+
873
+ def parse_cli_options(argv)
874
+ options = {}
875
+
876
+ parser = OptionParser.new do |opts|
877
+ opts.banner = <<~TEXT
878
+ Usage: stack.rb [app] [options]
879
+
880
+ Stack-first runtime surface:
881
+ stack.rb Start the mounted stack runtime
882
+ stack.rb dashboard Start one app directly
883
+
884
+ Canonical wrappers:
885
+ bin/start Start the mounted stack runtime
886
+ bin/console Open the igniter console
887
+ bin/dev Start all local node profiles
888
+ TEXT
889
+
890
+ opts.separator("")
891
+ opts.separator("Common flows:")
892
+ opts.separator(" --node NAME Boot one named node profile")
893
+ opts.separator(" --console Open the igniter console")
894
+ opts.separator(" --dev Start all local node profiles with prefixed logs (+ var/log/dev/*.log)")
895
+ opts.separator(" --print-compose Render compose config from stack nodes")
896
+ opts.separator(" --print-procfile-dev Render Procfile.dev from stack nodes")
897
+ opts.separator("")
898
+ opts.separator("Examples:")
899
+ opts.separator(" stack.rb")
900
+ opts.separator(" stack.rb dashboard")
901
+ opts.separator(" stack.rb --node seed")
902
+ opts.separator(" stack.rb --console --node seed")
903
+ opts.separator(" stack.rb --console --node seed --eval 'context.node_name'")
904
+ opts.separator(" stack.rb --dev")
905
+ opts.separator("")
906
+
907
+ opts.on("--app NAME", "Start a specific app by name") do |value|
908
+ options[:target] = value
909
+ end
910
+
911
+ opts.on("--node NAME", "Start a named local node profile") do |value|
912
+ options[:node] = value
913
+ end
914
+
915
+ opts.on("--console", "Start an interactive stack console (same surface as bin/console)") do
916
+ options[:console] = true
917
+ end
918
+
919
+ opts.on("-e", "--eval CODE", "Evaluate Ruby inside the stack console and exit") do |value|
920
+ options[:evaluate] = value
921
+ end
922
+
923
+ opts.on("--env NAME", "Use config/environments/<NAME>.yml overlay") do |value|
924
+ options[:environment] = value
925
+ end
926
+
927
+ opts.on("--print-compose", "Print a Docker Compose config generated from stack nodes") do
928
+ options[:print_compose] = true
929
+ end
930
+
931
+ opts.on("--write-compose [PATH]", "Write a Docker Compose config generated from stack nodes") do |value|
932
+ options[:write_compose] = value || true
933
+ end
934
+
935
+ opts.on("--dev", "Start stack nodes locally with prefixed logs and file logs") do
936
+ options[:dev] = true
937
+ end
938
+
939
+ opts.on("--print-procfile-dev", "Print a Procfile.dev generated from stack nodes") do
940
+ options[:print_procfile_dev] = true
941
+ end
942
+
943
+ opts.on("--write-procfile-dev [PATH]", "Write a Procfile.dev generated from stack nodes") do |value|
944
+ options[:write_procfile_dev] = value || true
945
+ end
946
+ end
947
+
948
+ parser.parse!(argv)
949
+ options[:target] ||= argv.shift
950
+ options
951
+ end
952
+
953
+ def resolve_app_name(name = nil)
954
+ return normalize_app_name(name) if name && app_names.include?(normalize_app_name(name))
955
+
956
+ normalize_app_name(name || root_app)
957
+ end
958
+
959
+ def resolve_node_name(name = nil)
960
+ raise ArgumentError, "No stack nodes configured" unless nodes_defined?
961
+
962
+ desired = normalize_app_name(name || default_node)
963
+ return desired if node_names.include?(desired)
964
+
965
+ available = node_names.map(&:inspect).join(", ")
966
+ raise ArgumentError, "Unknown stack node #{desired.inspect} (available: #{available})"
967
+ end
968
+
969
+ def normalize_app_name(name)
970
+ name.to_sym
971
+ end
972
+
973
+ def normalize_optional_name(name)
974
+ value = name.to_s.strip
975
+ value.empty? ? nil : value.to_sym
976
+ end
977
+
978
+ def nodes_defined?
979
+ !stack_settings.fetch("nodes", {}).empty?
980
+ end
981
+
982
+ def mounts_defined?
983
+ !@mounts.empty?
984
+ end
985
+
986
+ def resolved_environment
987
+ configured = @environment_name.to_s.strip
988
+ return configured unless configured.empty?
989
+
990
+ ENV["IGNITER_ENV"].to_s.strip
991
+ end
992
+
993
+ def environment_settings
994
+ @environment_settings ||= begin
995
+ env_name = resolved_environment
996
+ return {} if env_name.empty?
997
+
998
+ path = @environment_yml_path || File.join("config", "environments", "#{env_name}.yml")
999
+ load_yaml(resolve_path(path))
1000
+ end
1001
+ end
1002
+
1003
+ def app_deployment(name = nil)
1004
+ app_name = normalize_app_name(name || root_app)
1005
+ shared = stack_settings.fetch("shared", {})
1006
+ app = stack_settings.fetch("apps", {}).fetch(app_name.to_s, {})
1007
+ deep_merge(shared, app)
1008
+ end
1009
+
1010
+ def runtime_units_snapshot
1011
+ if nodes_defined?
1012
+ node_names.each_with_object({}) do |node_name, result|
1013
+ result[node_name.to_s] = node_deployment(node_name).merge(
1014
+ "node" => node_name.to_s,
1015
+ "default" => (node_name == default_node)
1016
+ )
1017
+ end
1018
+ else
1019
+ standalone_runtime_units_snapshot
1020
+ end
1021
+ end
1022
+
1023
+ def standalone_runtime_units_snapshot
1024
+ root_name = root_app.to_s
1025
+ units = {
1026
+ root_name => standalone_runtime_deployment(root_name).merge(
1027
+ "node" => root_name,
1028
+ "default" => true
1029
+ )
1030
+ }
1031
+
1032
+ ignition_plan.local_replica_intents.each do |intent|
1033
+ target = intent.target
1034
+ units[target.id] = local_replica_runtime_deployment(target, intent).merge(
1035
+ "node" => target.id,
1036
+ "default" => false
1037
+ )
1038
+ end
1039
+
1040
+ units
1041
+ end
1042
+
1043
+ def standalone_runtime_deployment(name)
1044
+ {
1045
+ "apps" => app_names.map(&:to_s),
1046
+ "root_app" => root_app.to_s,
1047
+ "mounts" => stringify_hash(mounts),
1048
+ "host" => stack_settings.dig("server", "host"),
1049
+ "port" => stack_settings.dig("server", "port"),
1050
+ "public" => true,
1051
+ "command" => "bundle exec ruby stack.rb",
1052
+ "role" => name.to_s,
1053
+ "environment" => shared_runtime_environment
1054
+ }
1055
+ end
1056
+
1057
+ def local_replica_runtime_deployment(target, intent)
1058
+ base = standalone_runtime_deployment(root_app.to_s)
1059
+ server_settings = target.server_settings
1060
+
1061
+ base.merge(
1062
+ "host" => server_settings["host"] || base["host"],
1063
+ "port" => server_settings["port"] || base["port"],
1064
+ "environment" => base.fetch("environment", {}).merge(
1065
+ "IGNITER_NODE" => target.id,
1066
+ "IGNITER_IGNITE_REPLICA" => "true",
1067
+ "IGNITER_IGNITE_TARGET" => target.id,
1068
+ "IGNITER_IGNITE_INTENT" => intent.id
1069
+ ),
1070
+ "ignite" => {
1071
+ "intent_id" => intent.id,
1072
+ "target_id" => target.id,
1073
+ "kind" => target.kind.to_s
1074
+ }
1075
+ )
1076
+ end
1077
+
1078
+ def node_deployment(name = nil)
1079
+ node_name = normalize_app_name(name || default_node)
1080
+ profile = deep_merge(stack_settings.fetch("shared", {}), node_profile(node_name))
1081
+ configured_mounts = stringify_hash(profile.fetch("mounts", {}))
1082
+
1083
+ {
1084
+ "apps" => app_names.map(&:to_s),
1085
+ "root_app" => root_app.to_s,
1086
+ "mounts" => configured_mounts.empty? ? stringify_hash(mounts) : configured_mounts,
1087
+ "host" => profile["host"] || stack_settings.dig("server", "host"),
1088
+ "port" => profile["port"] || profile.dig("http", "port") || stack_settings.dig("server", "port"),
1089
+ "public" => profile.key?("public") ? profile["public"] : true,
1090
+ "command" => profile["command"] || "bundle exec ruby stack.rb --node #{node_name}",
1091
+ "role" => profile["role"] || node_name.to_s,
1092
+ "depends_on" => Array(profile["depends_on"]).map(&:to_s),
1093
+ "environment" => shared_runtime_environment.merge(stringify_hash(profile.fetch("environment", {})))
1094
+ }
1095
+ end
1096
+
1097
+ def build_stack_runtime(node_name = nil)
1098
+ validate_interface_access!
1099
+ selected_node = nodes_defined? ? resolve_node_name(node_name) : nil
1100
+ root_app_name = root_app
1101
+ root_app_class = app_class(root_app_name)
1102
+ mounted_apps = mounted_stack_apps(selected_node)
1103
+ root_app_class.host_adapter.activate_transport!
1104
+ root_config = root_app_class.send(:build!)
1105
+ apply_http_settings!(root_config, stack_http_settings(selected_node))
1106
+ attach_ignite_runtime_hook!(root_config)
1107
+ root_config.custom_routes = mounted_apps.flat_map { |app| app.fetch(:routes) } + Array(root_config.custom_routes)
1108
+
1109
+ {
1110
+ runtime_name: selected_node || root_app_name,
1111
+ root_app_name: root_app_name,
1112
+ root_app_class: root_app_class,
1113
+ root_config: root_config,
1114
+ mounted_apps: mounted_apps
1115
+ }
1116
+ end
1117
+
1118
+ def mounted_stack_apps(node_name = nil)
1119
+ selected_mounts = if node_name && nodes_defined?
1120
+ node_deployment(node_name).fetch("mounts", {})
1121
+ else
1122
+ stringify_hash(mounts)
1123
+ end
1124
+
1125
+ selected_mounts.map do |app_name, mount_path|
1126
+ build_mounted_app(app_name: app_name, mount_path: mount_path)
1127
+ end
1128
+ end
1129
+
1130
+ def build_mounted_app(app_name:, mount_path:)
1131
+ normalized_app_name = normalize_app_name(app_name)
1132
+ klass = app_class(normalized_app_name)
1133
+
1134
+ klass.host_adapter.activate_transport!
1135
+ config = klass.send(:build!)
1136
+ rack_app = klass.host_adapter.rack_app(config: config)
1137
+
1138
+ {
1139
+ app_name: normalized_app_name,
1140
+ app_class: klass,
1141
+ config: config,
1142
+ rack_app: rack_app,
1143
+ mount_path: normalize_mount_path(mount_path),
1144
+ routes: mounted_app_routes(normalize_mount_path(mount_path), rack_app)
1145
+ }
1146
+ end
1147
+
1148
+ def mounted_app_routes(mount_path, rack_app)
1149
+ pattern = %r{\A#{Regexp.escape(mount_path)}(?<rest>/.*)?\z}
1150
+
1151
+ SERVICE_MOUNT_METHODS.map do |method|
1152
+ {
1153
+ method: method,
1154
+ path: pattern,
1155
+ handler: lambda do |params:, body:, headers:, env:, raw_body:, config:| # rubocop:disable Lint/UnusedBlockArgument
1156
+ forward_mounted_request(
1157
+ rack_app: rack_app,
1158
+ mount_path: mount_path,
1159
+ rest: params[:rest],
1160
+ env: env,
1161
+ raw_body: raw_body
1162
+ )
1163
+ end
1164
+ }
1165
+ end
1166
+ end
1167
+
1168
+ def forward_mounted_request(rack_app:, mount_path:, rest:, env:, raw_body:)
1169
+ forwarded_env = env.to_h.merge(
1170
+ "SCRIPT_NAME" => mount_path,
1171
+ "PATH_INFO" => normalize_forwarded_path(rest),
1172
+ "rack.input" => StringIO.new(raw_body.to_s)
1173
+ )
1174
+ status, headers, body = rack_app.call(forwarded_env)
1175
+ response_body = +""
1176
+ body.each { |chunk| response_body << chunk.to_s }
1177
+ body.close if body.respond_to?(:close)
1178
+
1179
+ {
1180
+ status: status.to_i,
1181
+ headers: headers,
1182
+ body: response_body
1183
+ }
1184
+ end
1185
+
1186
+ def normalize_forwarded_path(rest)
1187
+ value = rest.to_s
1188
+ value.empty? ? "/" : value
1189
+ end
1190
+
1191
+ def normalize_mount_path(path)
1192
+ value = path.to_s.strip
1193
+ value = "/#{value}" unless value.start_with?("/")
1194
+ value = value.sub(%r{/+\z}, "")
1195
+ value.empty? ? "/" : value
1196
+ end
1197
+
1198
+ def apply_http_settings!(config, http_settings)
1199
+ return config unless http_settings.is_a?(Hash)
1200
+
1201
+ config.host = http_settings["host"].to_s if present?(http_settings["host"])
1202
+ config.port = Integer(http_settings["port"]) if http_settings.key?("port") && !http_settings["port"].nil?
1203
+ config.log_format = http_settings["log_format"].to_sym if present?(http_settings["log_format"])
1204
+ config.drain_timeout = Integer(http_settings["drain_timeout"]) if http_settings.key?("drain_timeout") && !http_settings["drain_timeout"].nil?
1205
+ config
1206
+ end
1207
+
1208
+ def attach_ignite_runtime_hook!(config)
1209
+ context = ignite_runtime_boot_context
1210
+ return config unless context
1211
+ return config unless config.respond_to?(:after_start_hooks)
1212
+
1213
+ config.after_start_hooks << lambda do |config:, server:|
1214
+ complete_ignite_runtime_boot!(config: config, server: server, context: context)
1215
+ end
1216
+ config
1217
+ end
1218
+
1219
+ def ignite_runtime_boot_context
1220
+ target_id = ENV["IGNITER_IGNITE_TARGET"].to_s.strip
1221
+ return nil if target_id.empty?
1222
+
1223
+ {
1224
+ target_id: target_id,
1225
+ intent_id: ENV["IGNITER_IGNITE_INTENT"].to_s.strip,
1226
+ mode: ENV["IGNITER_IGNITE_MODE"].to_s.strip
1227
+ }
1228
+ end
1229
+
1230
+ def complete_ignite_runtime_boot!(config:, server:, context:)
1231
+ mesh = defined?(Igniter::Cluster::Mesh) ? Igniter::Cluster::Mesh : nil
1232
+ url = ignite_runtime_join_url(config, mesh: mesh)
1233
+ return if url.nil?
1234
+
1235
+ if mesh
1236
+ mesh.config.local_url = url if mesh.config.local_url.to_s.strip.empty?
1237
+ if mesh.config.seeds.any?
1238
+ Igniter::Cluster::Mesh::Announcer.new(mesh.config).announce_all
1239
+ elsif mesh.config.peer_name
1240
+ peer = Igniter::Cluster::Mesh::Peer.new(
1241
+ name: mesh.config.peer_name,
1242
+ url: url,
1243
+ capabilities: Array(mesh.config.local_capabilities),
1244
+ tags: Array(mesh.config.local_tags),
1245
+ metadata: Igniter::Cluster::Mesh::PeerMetadata.authoritative(
1246
+ mesh.config.local_metadata.merge(
1247
+ mesh_ignite: {
1248
+ target_id: context[:target_id],
1249
+ intent_id: context[:intent_id],
1250
+ mode: context[:mode],
1251
+ joined_at: Time.now.utc.iso8601
1252
+ }.compact
1253
+ ),
1254
+ origin: mesh.config.peer_name
1255
+ )
1256
+ )
1257
+ mesh.config.peer_registry.register(peer)
1258
+ end
1259
+
1260
+ mesh.config.governance_trail&.record(
1261
+ :ignite_runtime_joined,
1262
+ source: :stack_runtime,
1263
+ payload: {
1264
+ target_id: context[:target_id],
1265
+ intent_id: context[:intent_id],
1266
+ mode: context[:mode],
1267
+ url: url,
1268
+ server: server.class.name
1269
+ }.compact
1270
+ )
1271
+ end
1272
+
1273
+ { target_id: context[:target_id], url: url }
1274
+ end
1275
+
1276
+ def ignite_runtime_join_url(config, mesh:)
1277
+ local_url = mesh&.config&.local_url.to_s.strip
1278
+ return local_url unless local_url.empty?
1279
+
1280
+ host = config.host.to_s.strip
1281
+ host = "127.0.0.1" if host.empty? || host == "0.0.0.0"
1282
+ return nil if config.port.nil?
1283
+
1284
+ "http://#{host}:#{config.port}"
1285
+ end
1286
+
1287
+ def should_await_ignite_join?(report, mesh:, await_join:, bootstrap_remote:)
1288
+ enabled = await_join.nil? ? (mesh && bootstrap_remote) : await_join
1289
+ return false unless enabled
1290
+ return false unless mesh
1291
+ return false unless report.is_a?(Igniter::Ignite::IgnitionReport)
1292
+
1293
+ report.awaiting_join? || report.by_status.fetch(:bootstrapped, 0).positive?
1294
+ end
1295
+
1296
+ def await_ignite_join(report:, mesh:, timeout:, poll_interval:)
1297
+ deadline = Time.now + timeout.to_f
1298
+ current = report
1299
+
1300
+ loop do
1301
+ current = reconcile_ignite(report: current, mesh: mesh, persist: false)
1302
+ return current unless current.awaiting_join? || current.by_status.fetch(:bootstrapped, 0).positive?
1303
+ break if Time.now >= deadline
1304
+
1305
+ sleep poll_interval.to_f
1306
+ end
1307
+
1308
+ current
1309
+ end
1310
+
1311
+ def start_stack_schedulers(runtime)
1312
+ schedulers = []
1313
+ root_app_class = runtime.fetch(:root_app_class)
1314
+ root_config = runtime.fetch(:root_config)
1315
+ scheduler = root_app_class.send(:start_scheduler, root_config)
1316
+ schedulers << scheduler if scheduler
1317
+
1318
+ runtime.fetch(:mounted_apps).each do |mounted|
1319
+ scheduler = mounted.fetch(:app_class).send(:start_scheduler, mounted.fetch(:config))
1320
+ schedulers << scheduler if scheduler
1321
+ end
1322
+
1323
+ schedulers
1324
+ end
1325
+
1326
+ def stop_stack_schedulers(schedulers)
1327
+ Array(schedulers).each do |scheduler|
1328
+ scheduler&.stop
1329
+ end
1330
+ end
1331
+
1332
+ def build_config(context, dockerfile)
1333
+ config = {}
1334
+ config["context"] = context if present?(context)
1335
+ config["dockerfile"] = dockerfile if present?(dockerfile)
1336
+ config
1337
+ end
1338
+
1339
+ def runtime_environment_for_unit(unit_name, unit_config)
1340
+ env = stringify_hash(unit_config.fetch("environment", {}))
1341
+ env["IGNITER_NODE"] = unit_name.to_s if nodes_defined?
1342
+ env["IGNITER_ROOT_APP"] = unit_config["root_app"].to_s
1343
+ env["PORT"] = unit_config["port"].to_s if unit_config["port"]
1344
+ env["IGNITER_ENV"] = resolved_environment unless resolved_environment.empty?
1345
+ env.reject { |_key, value| !present?(value) }
1346
+ end
1347
+
1348
+ def dev_runtime_environment_for_unit(unit_name, unit_config)
1349
+ runtime_environment_for_unit(unit_name, unit_config).merge("RUBYOPT" => rubyopt_with_dev_output_sync)
1350
+ end
1351
+
1352
+ def rubyopt_with_dev_output_sync
1353
+ helper = File.expand_path("dev_output_sync", __dir__)
1354
+ parts = []
1355
+ existing = ENV["RUBYOPT"].to_s.strip
1356
+ parts << existing unless existing.empty?
1357
+ parts << "-r#{Shellwords.escape(helper)}"
1358
+ parts.join(" ").strip
1359
+ end
1360
+
1361
+ def shell_command_with_env(command, env)
1362
+ assignments = env.map do |key, value|
1363
+ "#{Shellwords.escape(key)}=#{Shellwords.escape(value)}"
1364
+ end
1365
+ (assignments + [command]).join(" ").strip
1366
+ end
1367
+
1368
+ def console_banner(context)
1369
+ [
1370
+ "Igniter Console",
1371
+ " stack=#{context.stack_class.name || "anonymous"}",
1372
+ " root_app=#{context.root_app_name}",
1373
+ " app=#{context.app_name}",
1374
+ " node=#{context.node_name || "none"}",
1375
+ " mounts=#{context.mounts.keys.map(&:to_s).join(", ")}",
1376
+ " helpers: stack, context, app, root_app, node, deployment, runtime, mesh"
1377
+ ].join("\n")
1378
+ end
1379
+
1380
+ def console_locals_binding(context)
1381
+ bind = Object.new.instance_eval { binding }
1382
+ {
1383
+ stack: context.stack_class,
1384
+ stack_class: context.stack_class,
1385
+ context: context,
1386
+ root_app: context.root_app_class,
1387
+ root_app_name: context.root_app_name,
1388
+ app: context.app_class,
1389
+ app_class: context.app_class,
1390
+ app_name: context.app_name,
1391
+ node: context.node_name,
1392
+ node_name: context.node_name,
1393
+ node_profile: context.node_profile,
1394
+ deployment: context.deployment,
1395
+ runtime: context.runtime,
1396
+ mounts: context.mounts,
1397
+ mesh: context.mesh,
1398
+ stack_settings: context.stack_settings
1399
+ }.each do |name, value|
1400
+ bind.local_variable_set(name, value)
1401
+ end
1402
+ bind
1403
+ end
1404
+
1405
+ def evaluate_console(bind, code, output)
1406
+ result = bind.eval(code)
1407
+ output.puts("=> #{result.inspect}")
1408
+ result
1409
+ end
1410
+
1411
+ def stringify_hash(hash)
1412
+ hash.each_with_object({}) do |(key, value), result|
1413
+ result[key.to_s] = value.to_s
1414
+ end
1415
+ end
1416
+
1417
+ def stringify_nested_keys(value)
1418
+ case value
1419
+ when Hash
1420
+ value.each_with_object({}) do |(key, nested_value), result|
1421
+ result[key.to_s] = stringify_nested_keys(nested_value)
1422
+ end
1423
+ when Array
1424
+ value.map { |item| stringify_nested_keys(item) }
1425
+ else
1426
+ value
1427
+ end
1428
+ end
1429
+
1430
+ def shared_runtime_environment
1431
+ stringify_hash(stack_settings.dig("shared", "environment") || {})
1432
+ end
1433
+
1434
+ def stack_slug
1435
+ name = stack_settings.dig("stack", "name")
1436
+ candidate = present?(name) ? name : File.basename(@root_dir.to_s)
1437
+ candidate.to_s.strip.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_+|_+\z/, "")
1438
+ end
1439
+
1440
+ def present?(value)
1441
+ !value.to_s.strip.empty?
1442
+ end
1443
+
1444
+ def use_stack_runtime_by_default?(name)
1445
+ name.nil? && mounts_defined?
1446
+ end
1447
+
1448
+ def stack_http_settings(node_name = nil)
1449
+ settings = stringify_hash(stack_settings.fetch("server", {}))
1450
+ if node_name && nodes_defined?
1451
+ settings = settings.merge(stringify_hash(node_profile(node_name).slice("host", "port", "log_format", "drain_timeout")))
1452
+ end
1453
+ settings["port"] = ENV["PORT"] if present?(ENV["PORT"])
1454
+ settings["port"] = settings["port"].to_i if settings["port"]
1455
+ settings
1456
+ end
1457
+
1458
+ def build_ignition_plan
1459
+ settings = ignite_settings
1460
+ mode = normalize_ignite_mode(settings["mode"])
1461
+ strategy = normalize_ignite_strategy(settings["strategy"])
1462
+ approval_mode = normalize_ignite_approval(settings["approval"])
1463
+ request_id = settings["request_id"] || "ignite-#{stack_slug}-#{resolved_environment.empty? ? "default" : resolved_environment}"
1464
+ requested_from = {
1465
+ "stack" => stack_slug,
1466
+ "environment" => resolved_environment,
1467
+ "root_app" => root_app.to_s
1468
+ }
1469
+ requested_by = {
1470
+ "kind" => "stack_config",
1471
+ "source" => "ignite"
1472
+ }
1473
+ seed_node = {
1474
+ "stack" => stack_slug,
1475
+ "root_app" => root_app.to_s,
1476
+ "host" => stack_settings.dig("server", "host"),
1477
+ "port" => stack_settings.dig("server", "port")
1478
+ }
1479
+
1480
+ intents = []
1481
+ Array(settings["replicas"]).each_with_index do |replica, index|
1482
+ intents << build_local_replica_intent(
1483
+ config: replica,
1484
+ index: index + 1,
1485
+ request_id: request_id,
1486
+ mode: mode,
1487
+ strategy: strategy,
1488
+ approval_mode: approval_mode,
1489
+ requested_by: requested_by,
1490
+ requested_from: requested_from,
1491
+ seed_node: seed_node
1492
+ )
1493
+ end
1494
+
1495
+ Array(settings["servers"]).each_with_index do |server, index|
1496
+ intents << build_remote_server_intent(
1497
+ config: server,
1498
+ index: index + 1,
1499
+ request_id: request_id,
1500
+ mode: mode,
1501
+ strategy: strategy,
1502
+ approval_mode: approval_mode,
1503
+ requested_by: requested_by,
1504
+ requested_from: requested_from,
1505
+ seed_node: seed_node
1506
+ )
1507
+ end
1508
+
1509
+ Igniter::Ignite::IgnitionPlan.new(
1510
+ id: request_id,
1511
+ ignite_mode: mode,
1512
+ strategy: strategy,
1513
+ approval_mode: approval_mode,
1514
+ intents: intents,
1515
+ requested_by: requested_by,
1516
+ requested_from: requested_from,
1517
+ seed_node: seed_node,
1518
+ metadata: {}
1519
+ )
1520
+ end
1521
+
1522
+ def build_local_replica_intent(config:, index:, request_id:, mode:, strategy:, approval_mode:, requested_by:, requested_from:, seed_node:)
1523
+ replica = Hash(config)
1524
+ replica_id = (replica["name"] || "replica-#{index}").to_s
1525
+ target = Igniter::Ignite::BootstrapTarget.new(
1526
+ id: replica_id,
1527
+ kind: :local_replica,
1528
+ locator: select_hash_keys(replica, %w[host port]),
1529
+ base_server: select_hash_keys(stack_settings.fetch("server", {}), %w[host port]),
1530
+ capability_intent: Array(replica["capabilities"]),
1531
+ bootstrap_requirements: Hash(replica["bootstrap"] || {}),
1532
+ metadata: reject_hash_keys(replica, %w[name host port capabilities bootstrap])
1533
+ )
1534
+
1535
+ Igniter::Ignite::DeploymentIntent.new(
1536
+ id: "#{request_id}-#{replica_id}",
1537
+ ignite_mode: mode,
1538
+ strategy: strategy,
1539
+ approval_mode: approval_mode,
1540
+ target: target,
1541
+ requested_capabilities: target.capability_intent,
1542
+ requested_by: requested_by,
1543
+ requested_from: requested_from,
1544
+ seed_node: seed_node,
1545
+ join_policy: {
1546
+ "admission" => "required",
1547
+ "trust" => "cluster_default"
1548
+ },
1549
+ correlation: {
1550
+ "ignite_request_id" => request_id,
1551
+ "target_id" => replica_id
1552
+ },
1553
+ metadata: {}
1554
+ )
1555
+ end
1556
+
1557
+ def build_remote_server_intent(config:, index:, request_id:, mode:, strategy:, approval_mode:, requested_by:, requested_from:, seed_node:)
1558
+ server = normalize_remote_server_config(config, index)
1559
+ server_id = server.fetch("id")
1560
+ target = Igniter::Ignite::BootstrapTarget.new(
1561
+ id: server_id,
1562
+ kind: :ssh_server,
1563
+ locator: { "config_path" => server.fetch("config_path") },
1564
+ base_server: select_hash_keys(stack_settings.fetch("server", {}), %w[host port]),
1565
+ capability_intent: Array(server["capabilities"]),
1566
+ bootstrap_requirements: Hash(server["bootstrap"] || {}),
1567
+ metadata: reject_hash_keys(server, %w[id config_path capabilities bootstrap])
1568
+ )
1569
+
1570
+ Igniter::Ignite::DeploymentIntent.new(
1571
+ id: "#{request_id}-#{server_id}",
1572
+ ignite_mode: mode,
1573
+ strategy: strategy,
1574
+ approval_mode: approval_mode,
1575
+ target: target,
1576
+ requested_capabilities: target.capability_intent,
1577
+ requested_by: requested_by,
1578
+ requested_from: requested_from,
1579
+ seed_node: seed_node,
1580
+ join_policy: {
1581
+ "admission" => "required",
1582
+ "trust" => "cluster_default"
1583
+ },
1584
+ correlation: {
1585
+ "ignite_request_id" => request_id,
1586
+ "target_id" => server_id
1587
+ },
1588
+ metadata: {}
1589
+ )
1590
+ end
1591
+
1592
+ def normalize_remote_server_config(config, index)
1593
+ case config
1594
+ when String
1595
+ {
1596
+ "id" => "server-#{index}",
1597
+ "config_path" => config
1598
+ }
1599
+ else
1600
+ hash = Hash(config)
1601
+ {
1602
+ "id" => (hash["name"] || hash["id"] || "server-#{index}").to_s,
1603
+ "config_path" => hash["target"] || hash.fetch("config_path"),
1604
+ "capabilities" => Array(hash["capabilities"]),
1605
+ "bootstrap" => Hash(hash["bootstrap"] || {})
1606
+ }.merge(reject_hash_keys(hash, %w[name id target config_path capabilities bootstrap]))
1607
+ end
1608
+ end
1609
+
1610
+ def normalize_ignite_mode(value)
1611
+ (value || "cold_start").to_sym
1612
+ end
1613
+
1614
+ def normalize_ignite_strategy(value)
1615
+ (value || "parallel").to_sym
1616
+ end
1617
+
1618
+ def normalize_ignite_approval(value)
1619
+ (value || "required").to_sym
1620
+ end
1621
+
1622
+ def select_hash_keys(hash, allowed_keys)
1623
+ Hash(hash).each_with_object({}) do |(key, value), result|
1624
+ result[key.to_s] = value if allowed_keys.include?(key.to_s)
1625
+ end
1626
+ end
1627
+
1628
+ def reject_hash_keys(hash, rejected_keys)
1629
+ Hash(hash).each_with_object({}) do |(key, value), result|
1630
+ result[key.to_s] = value unless rejected_keys.include?(key.to_s)
1631
+ end
1632
+ end
1633
+
1634
+ def default_runtime_command(name)
1635
+ if nodes_defined?
1636
+ "bundle exec ruby stack.rb --node #{name}"
1637
+ else
1638
+ "bundle exec ruby stack.rb"
1639
+ end
1640
+ end
1641
+
1642
+ def resolve_dev_log_dir
1643
+ configured = stack_settings.dig("development", "log_dir") || stack_settings.dig("dev", "log_dir")
1644
+ resolve_path(configured || "var/log/dev")
1645
+ end
1646
+
1647
+ def dev_log_path_for(name, dir: resolve_dev_log_dir)
1648
+ File.join(dir, "#{name}.log")
1649
+ end
1650
+
1651
+ def relative_to_root(path)
1652
+ return path unless @root_dir && path.start_with?(@root_dir.to_s)
1653
+
1654
+ path.delete_prefix(@root_dir.to_s).sub(%r{\A/}, "")
1655
+ end
1656
+
1657
+ def load_yaml(path)
1658
+ return {} unless path && File.exist?(path)
1659
+
1660
+ YAML.safe_load(File.read(path)) || {}
1661
+ end
1662
+
1663
+ def resolve_path(path)
1664
+ return nil if path.nil?
1665
+ return path if File.absolute_path(path) == path
1666
+
1667
+ File.expand_path(path, @root_dir || Dir.pwd)
1668
+ end
1669
+
1670
+ def default_ignition_store
1671
+ Igniter::Ignite::Stores::FileStore.new(
1672
+ path: resolve_path(File.join("var", "ignite", "#{stack_slug}.ndjson")),
1673
+ archive_path: resolve_path(File.join("var", "ignite", "#{stack_slug}.archive.ndjson"))
1674
+ )
1675
+ end
1676
+
1677
+ def default_credential_store
1678
+ Igniter::App::Credentials::Stores::FileStore.new(
1679
+ path: resolve_path(File.join("var", "credentials", "#{stack_slug}.ndjson")),
1680
+ archive_path: resolve_path(File.join("var", "credentials", "#{stack_slug}.archive.ndjson"))
1681
+ )
1682
+ end
1683
+
1684
+ def ignition_plan_for_target(target_id, mode: :expand)
1685
+ target = target_id.to_s
1686
+ base_plan = ignition_plan
1687
+ intent = base_plan.intents.find { |candidate| candidate.target.id == target }
1688
+ raise KeyError, "Unknown ignition target #{target.inspect}" unless intent
1689
+
1690
+ Igniter::Ignite::IgnitionPlan.new(
1691
+ id: "#{base_plan.id}:#{mode}:#{target}:#{Time.now.utc.strftime('%Y%m%d%H%M%S%6N')}",
1692
+ ignite_mode: mode,
1693
+ strategy: base_plan.strategy,
1694
+ approval_mode: base_plan.approval_mode,
1695
+ intents: [intent],
1696
+ requested_by: base_plan.requested_by,
1697
+ requested_from: base_plan.requested_from,
1698
+ seed_node: base_plan.seed_node,
1699
+ metadata: base_plan.metadata.merge("lifecycle_operation" => mode.to_s, "target_id" => target)
1700
+ )
1701
+ end
1702
+
1703
+ def persist_ignition_report!(report, source:)
1704
+ ignition_trail.ingest_report(report, source: source)
1705
+ end
1706
+
1707
+ def deep_merge(base, override)
1708
+ base.merge(override) do |_key, left, right|
1709
+ if left.is_a?(Hash) && right.is_a?(Hash)
1710
+ deep_merge(left, right)
1711
+ else
1712
+ right
1713
+ end
1714
+ end
1715
+ end
1716
+
1717
+ def reset_stack_state!
1718
+ @stack_settings = nil
1719
+ @environment_settings = nil
1720
+ @ignition_plan = nil
1721
+ @ignition_trail = nil
1722
+ @credential_trail = nil
1723
+ end
1724
+ end
1725
+ end
1726
+ end