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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/Rakefile +4 -4
- data/docs/v2/CHANGELOG.swarm_cli.md +9 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +19 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +45 -0
- data/docs/v2/guides/complete-tutorial.md +113 -1
- data/docs/v2/reference/ruby-dsl.md +138 -5
- data/docs/v2/reference/swarm_memory_technical_details.md +2090 -0
- data/lib/claude_swarm/cli.rb +9 -11
- data/lib/claude_swarm/commands/ps.rb +1 -2
- data/lib/claude_swarm/configuration.rb +2 -3
- data/lib/claude_swarm/orchestrator.rb +43 -44
- data/lib/claude_swarm/system_utils.rb +4 -4
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +4 -9
- data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
- data/lib/swarm_cli/config_loader.rb +11 -10
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_cli.rb +2 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
- data/lib/swarm_memory/core/storage.rb +66 -6
- data/lib/swarm_memory/integration/sdk_plugin.rb +14 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
- data/lib/swarm_memory/tools/memory_edit.rb +1 -0
- data/lib/swarm_memory/tools/memory_glob.rb +24 -1
- data/lib/swarm_memory/tools/memory_write.rb +2 -2
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +2 -0
- data/lib/swarm_sdk/agent/chat.rb +1 -1
- data/lib/swarm_sdk/agent/definition.rb +17 -1
- data/lib/swarm_sdk/node/agent_config.rb +7 -2
- data/lib/swarm_sdk/node/builder.rb +130 -35
- data/lib/swarm_sdk/node_context.rb +75 -0
- data/lib/swarm_sdk/node_orchestrator.rb +219 -12
- data/lib/swarm_sdk/plugin.rb +73 -1
- data/lib/swarm_sdk/result.rb +32 -6
- data/lib/swarm_sdk/swarm/builder.rb +1 -0
- data/lib/swarm_sdk/tools/delegate.rb +2 -2
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +3 -7
- data/memory/corpus-self-reflection/.lock +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.md +11 -0
- data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.md +20 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.md +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.md +18 -0
- data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.yml +21 -0
- data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.md +30 -0
- data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.md +21 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.md +18 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.md +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.md +17 -0
- data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.md +18 -0
- data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.md +15 -0
- data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.md +9 -0
- data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.md +13 -0
- data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.md +25 -0
- data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.md +26 -0
- data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.md +17 -0
- data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.md +14 -0
- data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.yml +21 -0
- data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.md +13 -0
- data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.md +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.md +39 -0
- data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.md +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.md +15 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.md +12 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.md +15 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.md +28 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.md +19 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.md +19 -0
- data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.yml +21 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.md +25 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.yml +22 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.emb +8 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.md +30 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.yml +22 -0
- data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.md +21 -0
- data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.yml +22 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.md +37 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.yml +8 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.md +24 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.yml +24 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.md +27 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.yml +24 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.md +26 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.yml +23 -0
- data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.md +37 -0
- data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.yml +25 -0
- data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.md +21 -0
- data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.yml +23 -0
- data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.md +25 -0
- data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.yml +8 -0
- data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.emb +0 -0
- data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.md +11 -0
- data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.yml +21 -0
- data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.emb +0 -0
- data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.md +19 -0
- data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.yml +21 -0
- data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.emb +0 -0
- data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.md +26 -0
- data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.yml +20 -0
- data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.emb +0 -0
- data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.md +23 -0
- data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.yml +21 -0
- data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.emb +0 -0
- data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.md +19 -0
- data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.yml +22 -0
- data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.emb +0 -0
- data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.md +21 -0
- data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.yml +22 -0
- data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.emb +0 -0
- data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.md +28 -0
- data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.yml +23 -0
- data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.emb +0 -0
- data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.md +21 -0
- data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.yml +20 -0
- data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.emb +0 -0
- data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.md +21 -0
- data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.yml +25 -0
- data/memory/corpus-self-reflection/experience/local-change-without-continuity.emb +0 -0
- data/memory/corpus-self-reflection/experience/local-change-without-continuity.md +30 -0
- data/memory/corpus-self-reflection/experience/local-change-without-continuity.yml +22 -0
- data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.emb +0 -0
- data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.md +21 -0
- data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.yml +20 -0
- data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.emb +0 -0
- data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.md +25 -0
- data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.yml +23 -0
- data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.emb +0 -0
- data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.md +32 -0
- data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.yml +22 -0
- data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.emb +0 -0
- data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.md +21 -0
- data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.yml +21 -0
- data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.emb +0 -0
- data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.md +21 -0
- data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.yml +21 -0
- data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.emb +0 -0
- data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.md +22 -0
- data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.yml +22 -0
- data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.emb +0 -0
- data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.md +28 -0
- data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.yml +24 -0
- data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.emb +0 -0
- data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.md +17 -0
- data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.yml +20 -0
- data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.emb +0 -0
- data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.md +9 -0
- data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.yml +22 -0
- metadata +172 -2
    
        data/lib/claude_swarm/cli.rb
    CHANGED
    
    | @@ -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 | 
            -
                  #  | 
| 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:  | 
| 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 | 
            -
                    #  | 
| 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 | 
            -
                         | 
| 556 | 
            -
                         | 
| 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 | 
            -
                       | 
| 562 | 
            +
                      Dir.pwd
         | 
| 565 563 | 
             
                    end
         | 
| 566 564 |  | 
| 567 | 
            -
                    config = Configuration.new(config_file, base_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. | 
| 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, : | 
| 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:  | 
| 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 | 
            -
                     | 
| 239 | 
            -
             | 
| 240 | 
            -
                       | 
| 241 | 
            -
             | 
| 242 | 
            -
             | 
| 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 | 
            -
                   | 
| 266 | 
            -
             | 
| 267 | 
            -
             | 
| 268 | 
            -
             | 
| 269 | 
            -
             | 
| 270 | 
            -
             | 
| 271 | 
            -
             | 
| 272 | 
            -
             | 
| 273 | 
            -
             | 
| 274 | 
            -
             | 
| 275 | 
            -
             | 
| 276 | 
            -
             | 
| 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 | 
            -
                   | 
| 340 | 
            -
                     | 
| 341 | 
            -
             | 
| 342 | 
            -
                    end
         | 
| 335 | 
            +
                  non_interactive_output do
         | 
| 336 | 
            +
                    print("⚙️  Executing after commands...")
         | 
| 337 | 
            +
                  end
         | 
| 343 338 |  | 
| 344 | 
            -
             | 
| 345 | 
            -
             | 
| 346 | 
            -
             | 
| 347 | 
            -
             | 
| 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,  | 
| 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" =>  | 
| 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 | 
            -
                       | 
| 843 | 
            -
                       | 
| 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: #{ | 
| 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: #{ | 
| 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?
         | 
    
        data/lib/claude_swarm/version.rb
    CHANGED
    
    
    
        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 = " | 
| 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  | 
| 18 | 
            -
                     | 
| 19 | 
            -
                    @scratchpad = SwarmSDK:: | 
| 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 | 
| 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  | 
| 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 | 
            -
                     | 
| 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
         | 
    
        data/lib/swarm_cli/version.rb
    CHANGED
    
    
    
        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 | 
            -
                     | 
| 104 | 
            -
                     | 
| 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 | 
            -
                   | 
| 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 | 
            -
             | 
| 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(
         | 
| @@ -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,
         | 
    
        data/lib/swarm_memory/version.rb
    CHANGED
    
    
    
        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
         |