magi-core 0.1.0

LLM-agnostic multi-perspective analysis system inspired by MAGI
Documentation
{
  "project": "magi-core",
  "description": "LLM-agnostic multi-perspective analysis system in Rust",
  "plan_source": "planning/claude-plan-tdd.md",
  "tasks": [
    {
      "id": "001",
      "title": "Error types foundation",
      "status": "completed",
      "priority": "high",
      "description": "MagiError and ProviderError enums with thiserror. Foundation for all other modules. Zero dependencies.",
      "section_ref": "planning/sections/section-01-errors.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "ProviderError: Http, Network, Timeout, Auth, Process, NestedSession — all with Display tests",
        "MagiError: Validation, InsufficientAgents, InputTooLarge, Deserialization, Io — all with Display tests",
        "From<ProviderError>, From<serde_json::Error>, From<std::io::Error> conversions",
        "No panic!, unwrap(), expect() outside #[cfg(test)]",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/error.rs",
      "impl_file": "src/error.rs"
    },
    {
      "id": "002",
      "title": "Domain schema types",
      "status": "completed",
      "priority": "high",
      "description": "Verdict, Severity, Mode, AgentName enums with encapsulated behavior. Finding and AgentOutput structs. Serialize/Deserialize/Clone/Debug/PartialEq/Eq/Hash.",
      "section_ref": "planning/sections/section-02-schema.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "Verdict: weight(), effective(), Display uppercase, serde lowercase, deserialization rejects invalid",
        "Severity: Ord (Critical>Warning>Info), icon(), Display uppercase, serde lowercase",
        "Mode: Display kebab-case, serde kebab-case, deserialization rejects invalid",
        "AgentName: title(), display_name(), Ord alphabetical, serde lowercase, usable as BTreeMap key",
        "Finding: stripped_title() removes zero-width Unicode, roundtrip serde",
        "AgentOutput: is_approving(), is_dissenting(), effective_verdict(), empty findings valid, ignores unknown fields",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/schema.rs",
      "impl_file": "src/schema.rs"
    },
    {
      "id": "003",
      "title": "Validation",
      "status": "completed",
      "priority": "high",
      "description": "Validator struct with ValidationLimits and precompiled Regex. Validates AgentOutput fields (confidence, text lengths, findings).",
      "section_ref": "planning/sections/section-03-validation.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "BDD-10: confidence out of range returns MagiError::Validation",
        "BDD-11: empty title after strip zero-width returns MagiError::Validation",
        "BDD-12: text field exceeds max_text_len returns MagiError::Validation with field name",
        "ValidationLimits::default() returns exact spec values (100, 500, 10_000, 50_000, 0.0, 1.0)",
        "Title length checked after strip_zero_width, not before",
        "NaN and infinity confidence rejected",
        "Validation order: confidence -> summary -> reasoning -> recommendation -> findings",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/validate.rs",
      "impl_file": "src/validate.rs"
    },
    {
      "id": "004",
      "title": "Consensus engine",
      "status": "completed",
      "priority": "high",
      "description": "ConsensusEngine with ConsensusConfig. Stateless determine() method. Score computation, epsilon-aware classification, finding deduplication, majority/dissent identification.",
      "section_ref": "planning/sections/section-04-consensus.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "BDD-01: 3 approve -> STRONG GO, score=1.0",
        "BDD-02: 2 approve + 1 reject -> GO (2-1), dissent contains rejector",
        "BDD-03: approve + conditional + reject -> GO WITH CAVEATS, conditions present",
        "BDD-04: 3 reject -> STRONG NO-GO, score=-1.0",
        "BDD-05: 1 approve + 1 reject (2 agents) -> HOLD -- TIE, verdict=Reject",
        "BDD-13: finding dedup by case-insensitive title, severity promoted, detail from highest",
        "BDD-33: degraded mode caps STRONG GO to GO (2-0), STRONG NO-GO to HOLD (2-0)",
        "Tiebreak by AgentName::cmp() Ord tested",
        "ConsensusConfig::default() returns min_agents=2, epsilon=1e-9",
        "Epsilon-aware classification near boundaries",
        "Confidence formula with weight_factor, clamped [0,1], rounded 2 decimals",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/consensus.rs",
      "impl_file": "src/consensus.rs"
    },
    {
      "id": "005",
      "title": "Reporting and MagiReport",
      "status": "completed",
      "priority": "high",
      "description": "ReportFormatter with ReportConfig. Fixed-width 52-char ASCII banner. Markdown report generation. MagiReport struct with Serialize.",
      "section_ref": "planning/sections/section-05-reporting.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "BDD-15: all banner lines exactly 52 characters wide",
        "BDD-16: mixed consensus report contains all 5 markdown headers",
        "Optional sections omitted when empty (dissent, conditions, findings)",
        "Report section order matches spec exactly",
        "MagiReport serializes to JSON matching Python original format",
        "Agent names lowercase in JSON, confidence rounded to 2 decimals",
        "failed_agents JSON behavior verified (omit or empty when not degraded)",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/reporting.rs",
      "impl_file": "src/reporting.rs"
    },
    {
      "id": "006",
      "title": "LlmProvider trait and RetryProvider",
      "status": "completed",
      "priority": "high",
      "description": "LlmProvider async trait (Send + Sync) with async-trait. CompletionConfig. RetryProvider opt-in wrapper.",
      "section_ref": "planning/sections/section-06-provider.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "CompletionConfig::default() has max_tokens=4096, temperature=0.0",
        "RetryProvider wraps inner provider, delegates name()/model()",
        "RetryProvider retries on Timeout, Http 500, Http 429, Network",
        "RetryProvider does NOT retry on Auth, Process, NestedSession, Http 4xx (except 429)",
        "RetryProvider returns last error after exhausting retries",
        "RetryProvider default: 3 retries, 1s delay",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/provider.rs",
      "impl_file": "src/provider.rs"
    },
    {
      "id": "007",
      "title": "Agents and AgentFactory",
      "status": "completed",
      "priority": "high",
      "description": "Agent struct with Arc<dyn LlmProvider>. AgentFactory with default and per-agent providers. System prompts from include_str! .md files (9 files: 3 agents x 3 modes).",
      "section_ref": "planning/sections/section-07-agents.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "BDD-26: each agent invokes its own provider (verified by mock call count)",
        "BDD-27: factory default provider for unoverridden, override for specified",
        "BDD-30: CodeReview, Design, Analysis produce distinct system prompts per agent",
        "BDD-31: from_file with nonexistent path returns MagiError::Io",
        "Default prompts contain JSON schema instructions and English-only constraint",
        "AgentFactory::create_agents returns agents in order [Melchior, Balthasar, Caspar]",
        "from_directory skips missing individual files silently, errors on missing directory",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/agent.rs",
      "impl_file": "src/agent.rs"
    },
    {
      "id": "008",
      "title": "Orchestrator",
      "status": "completed",
      "priority": "high",
      "description": "Magi struct as main entry point. MagiBuilder with consuming method chaining. MagiConfig. analyze() orchestrating full flow with JoinSet.",
      "section_ref": "planning/sections/section-08-orchestrator.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "BDD-01: analyze with 3 unanimous approve returns full MagiReport, degraded=false",
        "BDD-06: 2 succeed + 1 timeout -> Ok(MagiReport), degraded=true",
        "BDD-07: 2 succeed + 1 bad JSON -> Ok(MagiReport), degraded=true",
        "BDD-08: 1 succeed + 2 fail -> Err(InsufficientAgents { succeeded: 1, required: 2 })",
        "BDD-09: 0 succeed -> Err(InsufficientAgents { succeeded: 0, required: 2 })",
        "BDD-14: non-JSON agent response treated as failure, continues with remaining",
        "BDD-28: Magi::new with single provider creates with all defaults",
        "BDD-29: builder sets per-agent providers and custom timeout",
        "BDD-32: content exceeding max_input_len -> Err(InputTooLarge) without launching agents",
        "parse_agent_response strips code fences, finds JSON in preamble text",
        "Dropped analyze future aborts spawned agent tasks (JoinSet cancellation)",
        "MagiConfig::default timeout=300s, max_input_len=1MB",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/orchestrator.rs",
      "impl_file": "src/orchestrator.rs"
    },
    {
      "id": "009",
      "title": "ClaudeProvider HTTP API",
      "status": "completed",
      "priority": "medium",
      "description": "ClaudeProvider implementing LlmProvider via reqwest HTTP client. Claude Messages API. Feature-gated: claude-api.",
      "section_ref": "planning/sections/section-09-claude-api.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "ClaudeProvider::new creates provider with api_key and model",
        "provider.name() returns 'claude', provider.model() returns configured model",
        "POST to /v1/messages with x-api-key, anthropic-version headers",
        "Non-2xx response maps to ProviderError::Http",
        "Connection error maps to ProviderError::Network",
        "Request timeout maps to ProviderError::Timeout",
        "API key not exposed in Debug output",
        "reqwest::Client reused (connection pooling)",
        "cargo nextest run --features claude-api passes, all tests green"
      ],
      "test_file": "src/providers/claude.rs",
      "impl_file": "src/providers/claude.rs"
    },
    {
      "id": "010",
      "title": "ClaudeCliProvider CLI subprocess",
      "status": "completed",
      "priority": "medium",
      "description": "ClaudeCliProvider implementing LlmProvider via tokio::process. Model alias whitelist, CLAUDECODE env check, double-nested JSON parsing. Feature-gated: claude-cli.",
      "section_ref": "planning/sections/section-10-claude-cli.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "BDD-18: build_args includes --print, --output-format json, --model, --system-prompt",
        "BDD-19: parse_cli_output extracts inner JSON from double-nested envelope",
        "BDD-20: is_error=true returns ProviderError::Process",
        "BDD-21: extract_json strips code fences (```json and plain ```)",
        "BDD-22: timeout kills child process and returns ProviderError::Timeout",
        "BDD-23: CLAUDECODE env var present -> Err(ProviderError::NestedSession) in constructor",
        "Model aliases: sonnet, opus, haiku map correctly; pass-through for claude-* IDs",
        "Mixed case aliases (Sonnet, SONNET) rejected with ProviderError::Auth",
        "User prompt sent via stdin, not as CLI argument",
        "cargo nextest run --features claude-cli passes, all tests green"
      ],
      "test_file": "src/providers/claude_cli.rs",
      "impl_file": "src/providers/claude_cli.rs"
    },
    {
      "id": "011",
      "title": "Prelude and crate root",
      "status": "completed",
      "priority": "medium",
      "description": "Prelude module re-exporting common types. lib.rs with module declarations and feature-gated conditional compilation.",
      "section_ref": "planning/sections/section-11-prelude.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "Prelude re-exports: Magi, Mode, MagiReport, LlmProvider, CompletionConfig, MagiError, etc.",
        "Crate compiles with no features enabled (core only)",
        "Crate compiles with claude-api feature",
        "Crate compiles with claude-cli feature",
        "Crate compiles with all features enabled",
        "Feature flags conditionally compile provider modules",
        "cargo nextest run passes, all tests green"
      ],
      "test_file": "src/prelude.rs",
      "impl_file": "src/lib.rs"
    },
    {
      "id": "012",
      "title": "Example binary",
      "status": "completed",
      "priority": "low",
      "description": "Example binary using ClaudeCliProvider by default with CLI arg support for selecting other providers.",
      "section_ref": "planning/sections/section-12-example.md",
      "spec_ref": "sbtdd/spec-behavior.md",
      "acceptance_criteria": [
        "Example compiles with --features claude-cli",
        "Default provider is ClaudeCliProvider",
        "CLI args support provider selection (cli/api), model, and mode",
        "No unwrap() or expect() — uses ? operator throughout",
        "Provider-specific code behind #[cfg(feature)] gates",
        "cargo build --example basic_analysis --features claude-cli succeeds"
      ],
      "test_file": "examples/basic_analysis.rs",
      "impl_file": "examples/basic_analysis.rs"
    }
  ]
}