claude_swarm 1.0.4 → 1.0.5

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 (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Rakefile +4 -4
  4. data/docs/v2/CHANGELOG.swarm_cli.md +9 -0
  5. data/docs/v2/CHANGELOG.swarm_memory.md +19 -0
  6. data/docs/v2/CHANGELOG.swarm_sdk.md +45 -0
  7. data/docs/v2/guides/complete-tutorial.md +113 -1
  8. data/docs/v2/reference/ruby-dsl.md +138 -5
  9. data/docs/v2/reference/swarm_memory_technical_details.md +2090 -0
  10. data/lib/claude_swarm/cli.rb +9 -11
  11. data/lib/claude_swarm/commands/ps.rb +1 -2
  12. data/lib/claude_swarm/configuration.rb +2 -3
  13. data/lib/claude_swarm/orchestrator.rb +43 -44
  14. data/lib/claude_swarm/system_utils.rb +4 -4
  15. data/lib/claude_swarm/version.rb +1 -1
  16. data/lib/claude_swarm.rb +4 -9
  17. data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
  18. data/lib/swarm_cli/config_loader.rb +11 -10
  19. data/lib/swarm_cli/version.rb +1 -1
  20. data/lib/swarm_cli.rb +2 -0
  21. data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
  22. data/lib/swarm_memory/core/storage.rb +66 -6
  23. data/lib/swarm_memory/integration/sdk_plugin.rb +14 -0
  24. data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
  25. data/lib/swarm_memory/tools/memory_edit.rb +1 -0
  26. data/lib/swarm_memory/tools/memory_glob.rb +24 -1
  27. data/lib/swarm_memory/tools/memory_write.rb +2 -2
  28. data/lib/swarm_memory/version.rb +1 -1
  29. data/lib/swarm_memory.rb +2 -0
  30. data/lib/swarm_sdk/agent/chat.rb +1 -1
  31. data/lib/swarm_sdk/agent/definition.rb +17 -1
  32. data/lib/swarm_sdk/node/agent_config.rb +7 -2
  33. data/lib/swarm_sdk/node/builder.rb +130 -35
  34. data/lib/swarm_sdk/node_context.rb +75 -0
  35. data/lib/swarm_sdk/node_orchestrator.rb +219 -12
  36. data/lib/swarm_sdk/plugin.rb +73 -1
  37. data/lib/swarm_sdk/result.rb +32 -6
  38. data/lib/swarm_sdk/swarm/builder.rb +1 -0
  39. data/lib/swarm_sdk/tools/delegate.rb +2 -2
  40. data/lib/swarm_sdk/version.rb +1 -1
  41. data/lib/swarm_sdk.rb +3 -7
  42. data/memory/corpus-self-reflection/.lock +0 -0
  43. data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.emb +0 -0
  44. data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.md +11 -0
  45. data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.yml +23 -0
  46. data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.emb +0 -0
  47. data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.md +20 -0
  48. data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.yml +22 -0
  49. data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.emb +0 -0
  50. data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.md +24 -0
  51. data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.yml +22 -0
  52. data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.emb +0 -0
  53. data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.md +18 -0
  54. data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.yml +21 -0
  55. data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.emb +0 -0
  56. data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.md +30 -0
  57. data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.yml +8 -0
  58. data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.emb +0 -0
  59. data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.md +21 -0
  60. data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.yml +24 -0
  61. data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.emb +0 -0
  62. data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.md +18 -0
  63. data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.yml +24 -0
  64. data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.emb +0 -0
  65. data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.md +23 -0
  66. data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.yml +23 -0
  67. data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.emb +0 -0
  68. data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.md +17 -0
  69. data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.yml +22 -0
  70. data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.emb +0 -0
  71. data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.md +18 -0
  72. data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.yml +22 -0
  73. data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.emb +0 -0
  74. data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.md +15 -0
  75. data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.yml +22 -0
  76. data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.emb +0 -0
  77. data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.md +9 -0
  78. data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.yml +24 -0
  79. data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.emb +0 -0
  80. data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.md +13 -0
  81. data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.yml +24 -0
  82. data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.emb +0 -0
  83. data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.md +25 -0
  84. data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.yml +24 -0
  85. data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.emb +0 -0
  86. data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.md +26 -0
  87. data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.yml +24 -0
  88. data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.emb +0 -0
  89. data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.md +17 -0
  90. data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.yml +22 -0
  91. data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.emb +0 -0
  92. data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.md +14 -0
  93. data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.yml +21 -0
  94. data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.emb +0 -0
  95. data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.md +13 -0
  96. data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.yml +23 -0
  97. data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.emb +0 -0
  98. data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.md +22 -0
  99. data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.yml +23 -0
  100. data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.emb +0 -0
  101. data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.md +39 -0
  102. data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.yml +8 -0
  103. data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.emb +0 -0
  104. data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.md +23 -0
  105. data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.yml +24 -0
  106. data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.emb +0 -0
  107. data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.md +15 -0
  108. data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.yml +22 -0
  109. data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.emb +0 -0
  110. data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.md +12 -0
  111. data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.yml +23 -0
  112. data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.emb +0 -0
  113. data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.md +15 -0
  114. data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.yml +8 -0
  115. data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.emb +0 -0
  116. data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.md +28 -0
  117. data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.yml +8 -0
  118. data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.emb +0 -0
  119. data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.md +19 -0
  120. data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.yml +22 -0
  121. data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.emb +0 -0
  122. data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.md +19 -0
  123. data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.yml +21 -0
  124. data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.emb +0 -0
  125. data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.md +25 -0
  126. data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.yml +22 -0
  127. data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.emb +8 -0
  128. data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.md +30 -0
  129. data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.yml +22 -0
  130. data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.emb +0 -0
  131. data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.md +21 -0
  132. data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.yml +22 -0
  133. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.emb +0 -0
  134. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.md +37 -0
  135. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.yml +8 -0
  136. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.emb +0 -0
  137. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.md +24 -0
  138. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.yml +24 -0
  139. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.emb +0 -0
  140. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.md +27 -0
  141. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.yml +24 -0
  142. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.emb +0 -0
  143. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.md +26 -0
  144. data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.yml +23 -0
  145. data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.emb +0 -0
  146. data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.md +37 -0
  147. data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.yml +25 -0
  148. data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.emb +0 -0
  149. data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.md +21 -0
  150. data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.yml +23 -0
  151. data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.emb +0 -0
  152. data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.md +25 -0
  153. data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.yml +8 -0
  154. data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.emb +0 -0
  155. data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.md +11 -0
  156. data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.yml +21 -0
  157. data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.emb +0 -0
  158. data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.md +19 -0
  159. data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.yml +21 -0
  160. data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.emb +0 -0
  161. data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.md +26 -0
  162. data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.yml +20 -0
  163. data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.emb +0 -0
  164. data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.md +23 -0
  165. data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.yml +21 -0
  166. data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.emb +0 -0
  167. data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.md +19 -0
  168. data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.yml +22 -0
  169. data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.emb +0 -0
  170. data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.md +21 -0
  171. data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.yml +22 -0
  172. data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.emb +0 -0
  173. data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.md +28 -0
  174. data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.yml +23 -0
  175. data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.emb +0 -0
  176. data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.md +21 -0
  177. data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.yml +20 -0
  178. data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.emb +0 -0
  179. data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.md +21 -0
  180. data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.yml +25 -0
  181. data/memory/corpus-self-reflection/experience/local-change-without-continuity.emb +0 -0
  182. data/memory/corpus-self-reflection/experience/local-change-without-continuity.md +30 -0
  183. data/memory/corpus-self-reflection/experience/local-change-without-continuity.yml +22 -0
  184. data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.emb +0 -0
  185. data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.md +21 -0
  186. data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.yml +20 -0
  187. data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.emb +0 -0
  188. data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.md +25 -0
  189. data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.yml +23 -0
  190. data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.emb +0 -0
  191. data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.md +32 -0
  192. data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.yml +22 -0
  193. data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.emb +0 -0
  194. data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.md +21 -0
  195. data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.yml +21 -0
  196. data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.emb +0 -0
  197. data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.md +21 -0
  198. data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.yml +21 -0
  199. data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.emb +0 -0
  200. data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.md +22 -0
  201. data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.yml +22 -0
  202. data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.emb +0 -0
  203. data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.md +28 -0
  204. data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.yml +24 -0
  205. data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.emb +0 -0
  206. data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.md +17 -0
  207. data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.yml +20 -0
  208. data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.emb +0 -0
  209. data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.md +9 -0
  210. data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.yml +22 -0
  211. metadata +172 -2
@@ -43,9 +43,8 @@ module ClaudeSwarm
43
43
  type: :string,
44
44
  desc: "Root directory for resolving relative paths (defaults to current directory)"
45
45
  def start(config_file = nil)
46
- # Set root directory early so it's available to all components
47
- root_dir = options[:root_dir] || Dir.pwd
48
- ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
46
+ # Determine root directory for this session
47
+ root_dir = File.expand_path(options[:root_dir] || Dir.pwd)
49
48
 
50
49
  # Resolve config path relative to root directory
51
50
  config_path = config_file || "claude-swarm.yml"
@@ -71,7 +70,7 @@ module ClaudeSwarm
71
70
  end
72
71
 
73
72
  begin
74
- config = Configuration.new(config_path, base_dir: ClaudeSwarm.root_dir, options: options)
73
+ config = Configuration.new(config_path, base_dir: root_dir, options: options)
75
74
  generator = McpGenerator.new(config, vibe: options[:vibe])
76
75
  orchestrator = Orchestrator.new(
77
76
  config,
@@ -547,24 +546,23 @@ module ClaudeSwarm
547
546
  exit(1)
548
547
  end
549
548
 
550
- # Change to the original root directory if it exists
549
+ # Load the original root directory from session
551
550
  root_dir_file = File.join(session_path, "root_directory")
552
- if File.exist?(root_dir_file)
551
+ root_dir = if File.exist?(root_dir_file)
553
552
  original_dir = File.read(root_dir_file).strip
554
553
  if Dir.exist?(original_dir)
555
- Dir.chdir(original_dir)
556
- ENV["CLAUDE_SWARM_ROOT_DIR"] = original_dir
557
- say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
554
+ say("Using original directory: #{original_dir}", :green) unless options[:prompt]
555
+ original_dir
558
556
  else
559
557
  error("Original directory no longer exists: #{original_dir}")
560
558
  exit(1)
561
559
  end
562
560
  else
563
561
  # If no root_directory file, use current directory
564
- ENV["CLAUDE_SWARM_ROOT_DIR"] = Dir.pwd
562
+ Dir.pwd
565
563
  end
566
564
 
567
- config = Configuration.new(config_file, base_dir: ClaudeSwarm.root_dir)
565
+ config = Configuration.new(config_file, base_dir: root_dir)
568
566
 
569
567
  # Load session metadata if it exists to check for worktree info
570
568
  session_metadata_file = File.join(session_path, "session_metadata.json")
@@ -96,9 +96,8 @@ module ClaudeSwarm
96
96
  main_instance = config.dig("swarm", "main")
97
97
 
98
98
  # Get base directory from session metadata or root_directory file
99
- base_dir = ClaudeSwarm.root_dir
100
99
  root_dir_file = File.join(session_dir, "root_directory")
101
- base_dir = File.read(root_dir_file).strip if File.exist?(root_dir_file)
100
+ base_dir = File.exist?(root_dir_file) ? File.read(root_dir_file).strip : Dir.pwd
102
101
 
103
102
  # Get all directories - handle both string and array formats
104
103
  dir_config = config.dig("swarm", "instances", main_instance, "directory")
@@ -13,13 +13,12 @@ module ClaudeSwarm
13
13
  ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
14
14
  O_SERIES_MODEL_PATTERN = /^(o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?|gpt-5(-mini|-nano)?)$/
15
15
 
16
- attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :root_directory
16
+ attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :base_dir
17
17
 
18
18
  def initialize(config_path, base_dir: nil, options: {})
19
19
  @config_path = Pathname.new(config_path).expand_path
20
20
  @config_dir = @config_path.dirname
21
- @base_dir = base_dir || @config_dir
22
- @root_directory = @base_dir
21
+ @base_dir = base_dir || @config_dir.to_s
23
22
  @options = options
24
23
  load_and_validate
25
24
  end
@@ -38,7 +38,7 @@ module ClaudeSwarm
38
38
  @session_log_path = File.join(@session_path, "session.log")
39
39
  else
40
40
  # Generate new session path
41
- session_params = { working_dir: ClaudeSwarm.root_dir }
41
+ session_params = { working_dir: @config.base_dir }
42
42
  session_params[:session_id] = @provided_session_id if @provided_session_id
43
43
  @session_path = SessionPath.generate(**session_params)
44
44
  SessionPath.ensure_directory(@session_path)
@@ -49,7 +49,6 @@ module ClaudeSwarm
49
49
 
50
50
  end
51
51
  ENV["CLAUDE_SWARM_SESSION_PATH"] = @session_path
52
- ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
53
52
 
54
53
  # Initialize components that depend on session path
55
54
  @process_tracker = ProcessTracker.new(@session_path)
@@ -235,13 +234,11 @@ module ClaudeSwarm
235
234
  before_commands_dir = parent_dir
236
235
  end
237
236
 
238
- Dir.chdir(before_commands_dir) do
239
- success = execute_before_commands?(before_commands)
240
- unless success
241
- non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
242
- cleanup_all
243
- exit(1)
244
- end
237
+ success = execute_before_commands?(before_commands, chdir: before_commands_dir)
238
+ unless success
239
+ non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
240
+ cleanup_all
241
+ exit(1)
245
242
  end
246
243
 
247
244
  non_interactive_output do
@@ -262,19 +259,18 @@ module ClaudeSwarm
262
259
  end
263
260
 
264
261
  # Execute the main instance - this will cascade to other instances via MCP
265
- Dir.chdir(main_instance[:directory]) do
266
- # Execute main Claude instance with unbundled environment to avoid bundler conflicts
267
- # This ensures the main instance runs in a clean environment without inheriting
268
- # Claude Swarm's BUNDLE_* environment variables
269
- Bundler.with_unbundled_env do
270
- if @non_interactive_prompt
271
- stream_to_session_log(*command)
272
- else
273
- system_with_pid!(*command) do |pid|
274
- @process_tracker.track_pid(pid, "claude_#{@config.main_instance}")
275
- non_interactive_output do
276
- puts "✓ Claude instance started with PID: #{pid}"
277
- end
262
+ # Execute main Claude instance with unbundled environment to avoid bundler conflicts
263
+ # This ensures the main instance runs in a clean environment without inheriting
264
+ # Claude Swarm's BUNDLE_* environment variables
265
+ main_dir = main_instance[:directory]
266
+ Bundler.with_unbundled_env do
267
+ if @non_interactive_prompt
268
+ stream_to_session_log(*command, chdir: main_dir)
269
+ else
270
+ system_with_pid!(*command, chdir: main_dir) do |pid|
271
+ @process_tracker.track_pid(pid, "claude_#{@config.main_instance}")
272
+ non_interactive_output do
273
+ puts "✓ Claude instance started with PID: #{pid}"
278
274
  end
279
275
  end
280
276
  end
@@ -306,12 +302,12 @@ module ClaudeSwarm
306
302
  puts
307
303
  end
308
304
 
309
- def execute_before_commands?(commands)
310
- execute_commands(commands, phase: "before", fail_fast: true)
305
+ def execute_before_commands?(commands, chdir:)
306
+ execute_commands(commands, phase: "before", fail_fast: true, chdir: chdir)
311
307
  end
312
308
 
313
- def execute_after_commands?(commands)
314
- execute_commands(commands, phase: "after", fail_fast: false)
309
+ def execute_after_commands?(commands, chdir:)
310
+ execute_commands(commands, phase: "after", fail_fast: false, chdir: chdir)
315
311
  end
316
312
 
317
313
  def execute_after_commands_once
@@ -336,16 +332,14 @@ module ClaudeSwarm
336
332
  after_commands_dir = parent_dir
337
333
  end
338
334
 
339
- Dir.chdir(after_commands_dir) do
340
- non_interactive_output do
341
- print("⚙️ Executing after commands...")
342
- end
335
+ non_interactive_output do
336
+ print("⚙️ Executing after commands...")
337
+ end
343
338
 
344
- success = execute_after_commands?(after_commands)
345
- unless success
346
- non_interactive_output do
347
- puts "⚠️ Some after commands failed"
348
- end
339
+ success = execute_after_commands?(after_commands, chdir: after_commands_dir)
340
+ unless success
341
+ non_interactive_output do
342
+ puts "⚠️ Some after commands failed"
349
343
  end
350
344
  end
351
345
  end
@@ -357,7 +351,7 @@ module ClaudeSwarm
357
351
 
358
352
  # Save the root directory
359
353
  root_dir_file = File.join(session_path, "root_directory")
360
- File.write(root_dir_file, ClaudeSwarm.root_dir)
354
+ File.write(root_dir_file, @config.base_dir)
361
355
 
362
356
  # Save session metadata
363
357
  metadata_file = File.join(session_path, "session_metadata.json")
@@ -366,7 +360,7 @@ module ClaudeSwarm
366
360
 
367
361
  def build_session_metadata
368
362
  {
369
- "root_directory" => ClaudeSwarm.root_dir,
363
+ "root_directory" => @config.base_dir,
370
364
  "timestamp" => Time.now.utc.iso8601,
371
365
  "start_time" => @start_time.utc.iso8601,
372
366
  "swarm_name" => @config.swarm_name,
@@ -642,12 +636,12 @@ module ClaudeSwarm
642
636
  end
643
637
  end
644
638
 
645
- def stream_to_session_log(*command)
639
+ def stream_to_session_log(*command, chdir:)
646
640
  # Setup logger for session logging
647
641
  logger = Logger.new(@session_log_path, level: :info, progname: @config.main_instance)
648
642
 
649
643
  # Use Open3.popen2e to capture stdout and stderr merged for formatting
650
- Open3.popen2e(*command) do |stdin, stdout_and_stderr, wait_thr|
644
+ Open3.popen2e(*command, chdir: chdir) do |stdin, stdout_and_stderr, wait_thr|
651
645
  stdin.close
652
646
 
653
647
  # Read and process the merged output
@@ -819,7 +813,10 @@ module ClaudeSwarm
819
813
  @logger ||= Logger.new(File.join(@session_path, "session.log"), level: :info, progname: "orchestrator")
820
814
  end
821
815
 
822
- def execute_commands(commands, phase:, fail_fast:)
816
+ def execute_commands(commands, phase:, fail_fast:, chdir:)
817
+ raise ArgumentError, "chdir parameter is required" if chdir.nil?
818
+ raise ArgumentError, "chdir must be a valid directory: #{chdir}" unless File.directory?(chdir)
819
+
823
820
  all_succeeded = true
824
821
 
825
822
  # Setup logger for session logging if we have a session path
@@ -839,13 +836,15 @@ module ClaudeSwarm
839
836
  end
840
837
  end
841
838
 
842
- output = %x(#{command} 2>&1)
843
- success = $CHILD_STATUS.success?
839
+ # Use Open3.capture2e with chdir option to execute the command
840
+ # This allows setting the working directory without changing the process directory
841
+ output, status = Open3.capture2e(command, chdir: chdir)
842
+ success = status.success?
844
843
  output_separator = "-" * 80
845
844
 
846
845
  logger.info { "Command output:" }
847
846
  logger.info { output }
848
- logger.info { "Exit status: #{$CHILD_STATUS.exitstatus}" }
847
+ logger.info { "Exit status: #{status.exitstatus}" }
849
848
  logger.info { output_separator }
850
849
 
851
850
  # Show output if in debug mode or if command failed
@@ -854,7 +853,7 @@ module ClaudeSwarm
854
853
  output_prefix = phase == "after" ? "After command" : "Command"
855
854
  puts "#{output_prefix} #{index + 1} output:"
856
855
  puts output
857
- print("Exit status: #{$CHILD_STATUS.exitstatus}")
856
+ print("Exit status: #{status.exitstatus}")
858
857
  end
859
858
  end
860
859
 
@@ -2,14 +2,14 @@
2
2
 
3
3
  module ClaudeSwarm
4
4
  module SystemUtils
5
- def system!(*args)
6
- system(*args)
5
+ def system!(*args, **options)
6
+ system(*args, **options)
7
7
  handle_command_failure(last_status, args)
8
8
  end
9
9
 
10
- def system_with_pid!(*args)
10
+ def system_with_pid!(*args, **options)
11
11
  # Spawn the process - by default, inherits the parent's I/O
12
- pid = Process.spawn(*args)
12
+ pid = Process.spawn(*args, **options)
13
13
 
14
14
  # Yield the PID to the block if given
15
15
  yield(pid) if block_given?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "1.0.4"
4
+ VERSION = "1.0.5"
5
5
  end
data/lib/claude_swarm.rb CHANGED
@@ -30,22 +30,20 @@ require_relative "claude_swarm/version"
30
30
  # Zeitwerk setup
31
31
  require "zeitwerk"
32
32
  loader = Zeitwerk::Loader.new
33
- loader.tag = "claude_swarm"
34
-
33
+ loader.tag = File.basename(__FILE__, ".rb")
35
34
  loader.ignore("#{__dir__}/claude_swarm/templates")
35
+ loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
36
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
36
37
  loader.inflector.inflect(
37
38
  "cli" => "CLI",
38
39
  "openai" => "OpenAI",
39
40
  )
41
+ loader.setup
40
42
 
41
43
  module ClaudeSwarm
42
44
  class Error < StandardError; end
43
45
 
44
46
  class << self
45
- def root_dir
46
- ENV.fetch("CLAUDE_SWARM_ROOT_DIR") { Dir.pwd }
47
- end
48
-
49
47
  def home_dir
50
48
  ENV.fetch("CLAUDE_SWARM_HOME") { File.expand_path("~/.claude-swarm") }
51
49
  end
@@ -67,6 +65,3 @@ module ClaudeSwarm
67
65
  end
68
66
  end
69
67
  end
70
-
71
- loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
72
- loader.setup
@@ -14,9 +14,9 @@ module SwarmCLI
14
14
 
15
15
  def initialize(options)
16
16
  @options = options
17
- # Create scratchpad with persistence for MCP server
18
- scratchpad_path = File.join(Dir.pwd, ".swarm", "scratchpad.json")
19
- @scratchpad = SwarmSDK::Scratchpad.new(persist_to: scratchpad_path)
17
+ # Create volatile scratchpad for MCP server
18
+ # Note: Scratchpad is always volatile - data is not persisted between sessions
19
+ @scratchpad = SwarmSDK::Tools::Stores::ScratchpadStorage.new
20
20
  end
21
21
 
22
22
  def execute
@@ -5,7 +5,7 @@ module SwarmCLI
5
5
  #
6
6
  # Supports:
7
7
  # - YAML files (.yml, .yaml) - loaded via SwarmSDK::Swarm.load
8
- # - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm instance
8
+ # - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
9
9
  #
10
10
  # @example Load YAML config
11
11
  # swarm = ConfigLoader.load("config.yml")
@@ -19,10 +19,10 @@ module SwarmCLI
19
19
  #
20
20
  # Detects file type by extension:
21
21
  # - .yml, .yaml -> Load as YAML using SwarmSDK::Swarm.load
22
- # - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm instance
22
+ # - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
23
23
  #
24
24
  # @param path [String, Pathname] Path to configuration file
25
- # @return [SwarmSDK::Swarm] Configured swarm instance
25
+ # @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
26
26
  # @raise [SwarmCLI::ConfigurationError] If file not found or invalid format
27
27
  def load(path)
28
28
  path = Pathname.new(path).expand_path
@@ -59,12 +59,12 @@ module SwarmCLI
59
59
  # Load Ruby DSL configuration file
60
60
  #
61
61
  # Executes the Ruby file in a clean binding and expects it to return
62
- # a SwarmSDK::Swarm instance. The file should use SwarmSDK.build or
63
- # create a Swarm instance directly.
62
+ # a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. The file should
63
+ # use SwarmSDK.build or create a Swarm/NodeOrchestrator instance directly.
64
64
  #
65
65
  # @param path [Pathname] Path to Ruby DSL file
66
- # @return [SwarmSDK::Swarm] Configured swarm instance
67
- # @raise [ConfigurationError] If file doesn't return a Swarm instance
66
+ # @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
67
+ # @raise [ConfigurationError] If file doesn't return a valid instance
68
68
  def load_ruby_dsl(path)
69
69
  # Read the file content
70
70
  content = path.read
@@ -73,10 +73,11 @@ module SwarmCLI
73
73
  # This allows the DSL file to use SwarmSDK.build directly
74
74
  result = eval(content, binding, path.to_s, 1) # rubocop:disable Security/Eval
75
75
 
76
- # Validate result is a Swarm instance
77
- unless result.is_a?(SwarmSDK::Swarm)
76
+ # Validate result is a Swarm or NodeOrchestrator instance
77
+ # Both have the same execute(prompt) interface
78
+ unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::NodeOrchestrator)
78
79
  raise ConfigurationError,
79
- "Ruby DSL file must return a SwarmSDK::Swarm instance. " \
80
+ "Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. " \
80
81
  "Got: #{result.class}. " \
81
82
  "Use: SwarmSDK.build { ... } or Swarm.new(...)"
82
83
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.1.0"
4
+ VERSION = "2.1.1"
5
5
  end
data/lib/swarm_cli.rb CHANGED
@@ -22,7 +22,9 @@ require_relative "swarm_cli/version"
22
22
 
23
23
  require "zeitwerk"
24
24
  loader = Zeitwerk::Loader.new
25
+ loader.tag = File.basename(__FILE__, ".rb")
25
26
  loader.push_dir("#{__dir__}/swarm_cli", namespace: SwarmCLI)
27
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
26
28
  loader.inflector.inflect(
27
29
  "cli" => "CLI",
28
30
  "ui" => "UI",
@@ -171,12 +171,6 @@ module SwarmMemory
171
171
 
172
172
  content = File.read(md_file)
173
173
 
174
- # Check if it's a stub (redirect)
175
- if stub_content?(content)
176
- target_path = extract_redirect_target(content)
177
- return read(file_path: target_path) if target_path
178
- end
179
-
180
174
  # Increment hit counter
181
175
  increment_hits(file_path)
182
176
 
@@ -205,12 +199,6 @@ module SwarmMemory
205
199
 
206
200
  content = File.read(md_file)
207
201
 
208
- # Follow stub redirect if applicable
209
- if stub_content?(content)
210
- target_path = extract_redirect_target(content)
211
- return read_entry(file_path: target_path) if target_path
212
- end
213
-
214
202
  # Read metadata
215
203
  yaml_data = File.exist?(yaml_file) ? YAML.load_file(yaml_file, permitted_classes: [Time, Date, Symbol]) : {}
216
204
 
@@ -95,22 +95,82 @@ module SwarmMemory
95
95
  )
96
96
  end
97
97
 
98
- # Read content from storage
98
+ # Read content from storage, automatically following stub redirects
99
99
  #
100
100
  # @param file_path [String] Path to read from
101
101
  # @return [String] Content at the path
102
102
  def read(file_path:)
103
- normalized_path = PathNormalizer.normalize(file_path)
104
- @adapter.read(file_path: normalized_path)
103
+ entry = read_entry(file_path: file_path)
104
+ entry.content
105
105
  end
106
106
 
107
- # Read full entry with metadata
107
+ # Read full entry with metadata, automatically following stub redirects
108
+ #
109
+ # Stub redirects are created by MemoryDefrag when merging/moving entries.
110
+ # This method transparently follows redirect chains up to 5 levels deep.
108
111
  #
109
112
  # @param file_path [String] Path to read from
113
+ # @param visited [Array<String>] Internal: tracks visited paths to detect circular redirects
110
114
  # @return [Entry] Full entry object
111
- def read_entry(file_path:)
115
+ # @raise [ArgumentError] If path not found, circular redirect detected, or too many redirects
116
+ def read_entry(file_path:, visited: [])
112
117
  normalized_path = PathNormalizer.normalize(file_path)
113
- @adapter.read_entry(file_path: normalized_path)
118
+
119
+ # Detect circular redirects immediately
120
+ if visited.include?(normalized_path)
121
+ cycle = visited + [normalized_path]
122
+ raise ArgumentError,
123
+ "Circular redirect detected in memory storage: #{cycle.join(" → ")}\n\n" \
124
+ "This indicates corrupted stub files. Please run MemoryDefrag to repair:\n " \
125
+ "MemoryDefrag(action: \"analyze\")"
126
+ end
127
+
128
+ # Check depth limit (prevent infinite chains)
129
+ if visited.size >= 5
130
+ chain = visited + [normalized_path]
131
+ raise ArgumentError,
132
+ "Memory redirect chain too deep (>5 redirects): #{chain.join(" → ")}\n\n" \
133
+ "This indicates fragmented memory storage. Please run maintenance:\n " \
134
+ "MemoryDefrag(action: \"full\", dry_run: true) # Preview first\n " \
135
+ "MemoryDefrag(action: \"full\", dry_run: false) # Execute"
136
+ end
137
+
138
+ # Read entry from adapter
139
+ begin
140
+ entry = @adapter.read_entry(file_path: normalized_path)
141
+ rescue ArgumentError
142
+ # If this is a redirect target that doesn't exist, provide helpful error
143
+ if visited.empty?
144
+ # Not a redirect, just re-raise original error
145
+ raise
146
+ else
147
+ original_path = visited.first
148
+ raise ArgumentError,
149
+ "memory://#{original_path} was redirected to memory://#{normalized_path}, but the target was not found.\n\n" \
150
+ "The original entry may have been merged or moved incorrectly. " \
151
+ "Run MemoryDefrag to identify and fix broken redirects:\n " \
152
+ "MemoryDefrag(action: \"analyze\")"
153
+ end
154
+ end
155
+
156
+ # Check if this is a stub redirect
157
+ if entry.metadata && entry.metadata["stub"] == true
158
+ redirect_target = entry.metadata["redirect_to"]
159
+
160
+ # Validate redirect target exists
161
+ if redirect_target.nil? || redirect_target.strip.empty?
162
+ raise ArgumentError,
163
+ "memory://#{normalized_path} is a stub with invalid redirect metadata.\n\n" \
164
+ "This should never happen (stubs are created by MemoryDefrag). " \
165
+ "The stub file may be corrupted. Please report this as a bug."
166
+ end
167
+
168
+ # Follow redirect recursively, tracking visited paths
169
+ return read_entry(file_path: redirect_target, visited: visited + [normalized_path])
170
+ end
171
+
172
+ # Not a stub, return the entry
173
+ entry
114
174
  end
115
175
 
116
176
  # Delete an entry
@@ -207,6 +207,19 @@ module SwarmMemory
207
207
  agent_definition.memory_enabled?
208
208
  end
209
209
 
210
+ # Contribute to agent serialization
211
+ #
212
+ # Preserves memory configuration when agents are cloned (e.g., in NodeOrchestrator).
213
+ # This allows memory configuration to persist across node transitions.
214
+ #
215
+ # @param agent_definition [Agent::Definition] Agent definition
216
+ # @return [Hash] Memory config to include in to_h
217
+ def serialize_config(agent_definition:)
218
+ return {} unless agent_definition.memory
219
+
220
+ { memory: agent_definition.memory }
221
+ end
222
+
210
223
  # Lifecycle: Agent initialized
211
224
  #
212
225
  # Filters tools by mode (removing non-mode tools), registers LoadSkill,
@@ -287,6 +300,7 @@ module SwarmMemory
287
300
  def on_user_message(agent_name:, prompt:, is_first_message:)
288
301
  storage = @storages[agent_name]
289
302
  return [] unless storage&.semantic_index
303
+ return [] if prompt.empty?
290
304
 
291
305
  # Adaptive threshold based on query length
292
306
  # Short queries use lower threshold as they have less semantic richness
@@ -747,7 +747,11 @@ module SwarmMemory
747
747
  # @param to [String] Target path
748
748
  # @param reason [String] Reason (merged, moved)
749
749
  # @return [void]
750
+ # @raise [ArgumentError] If target path or reason is nil/empty
750
751
  def create_stub(from:, to:, reason:)
752
+ raise ArgumentError, "Cannot create stub without target path" if to.nil? || to.strip.empty?
753
+ raise ArgumentError, "Cannot create stub without reason" if reason.nil? || reason.strip.empty?
754
+
751
755
  stub_content = "# #{reason} → #{to}\n\nThis entry was #{reason} into #{to}."
752
756
 
753
757
  @adapter.write(
@@ -85,6 +85,7 @@ module SwarmMemory
85
85
 
86
86
  param :replace_all,
87
87
  desc: "Replace all occurrences of old_string (default false)",
88
+ type: :boolean,
88
89
  required: false
89
90
 
90
91
  # Initialize with storage instance and agent name
@@ -100,6 +100,8 @@ module SwarmMemory
100
100
  desc: "Glob pattern - target concept/, fact/, skill/, or experience/ only (e.g., 'skill/**', 'concept/ruby/*', 'fact/people/*.md')",
101
101
  required: true
102
102
 
103
+ MAX_RESULTS = 500 # Limit results to prevent overwhelming output
104
+
103
105
  # Initialize with storage instance
104
106
  #
105
107
  # @param storage [Core::Storage] Storage instance
@@ -124,6 +126,14 @@ module SwarmMemory
124
126
  return "No entries found matching pattern '#{pattern}'"
125
127
  end
126
128
 
129
+ # Limit results
130
+ if entries.count > MAX_RESULTS
131
+ entries = entries.take(MAX_RESULTS)
132
+ truncated = true
133
+ else
134
+ truncated = false
135
+ end
136
+
127
137
  result = []
128
138
  result << "Memory entries matching '#{pattern}' (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
129
139
 
@@ -131,7 +141,20 @@ module SwarmMemory
131
141
  result << " memory://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
132
142
  end
133
143
 
134
- result.join("\n")
144
+ output = result.join("\n")
145
+
146
+ # Add system reminder if truncated
147
+ if truncated
148
+ output += <<~REMINDER
149
+
150
+ <system-reminder>
151
+ Results limited to first #{MAX_RESULTS} matches (sorted by most recently modified).
152
+ Consider using a more specific pattern to narrow your search.
153
+ </system-reminder>
154
+ REMINDER
155
+ end
156
+
157
+ output
135
158
  rescue ArgumentError => e
136
159
  validation_error(e.message)
137
160
  end
@@ -45,8 +45,8 @@ module SwarmMemory
45
45
  TAGS ARE CRITICAL: Think "What would I search for in 6 months?" For skills especially, be VERY comprehensive with tags - they're your search index.
46
46
 
47
47
  EXAMPLES:
48
- - For concept: tags: ['ruby', 'oop', 'classes', 'inheritance', 'methods']
49
- - For skill: tags: ['debugging', 'api', 'http', 'errors', 'trace', 'network', 'rest']
48
+ - For concept: tags: (JSON) "['ruby', 'oop', 'classes', 'inheritance', 'methods']"
49
+ - For skill: tags: (JSON) "['debugging', 'api', 'http', 'errors', 'trace', 'network', 'rest']"
50
50
  DESC
51
51
 
52
52
  param :file_path,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmMemory
4
- VERSION = "2.1.1"
4
+ VERSION = "2.1.2"
5
5
  end
data/lib/swarm_memory.rb CHANGED
@@ -28,7 +28,9 @@ require_relative "swarm_memory/version"
28
28
  # Setup Zeitwerk loader
29
29
  require "zeitwerk"
30
30
  loader = Zeitwerk::Loader.new
31
+ loader.tag = File.basename(__FILE__, ".rb")
31
32
  loader.push_dir("#{__dir__}/swarm_memory", namespace: SwarmMemory)
33
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
32
34
  loader.setup
33
35
 
34
36
  # Explicitly load DSL components and extensions to inject into SwarmSDK