{"id":49956276,"url":"https://github.com/launchapp-dev/animus-protocol","last_synced_at":"2026-06-07T17:00:23.338Z","repository":{"id":358552123,"uuid":"1241805111","full_name":"launchapp-dev/animus-protocol","owner":"launchapp-dev","description":"Plugin protocol stack for Animus — SDK crates and spec, coming v0.4.0","archived":false,"fork":false,"pushed_at":"2026-06-05T02:44:09.000Z","size":480,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T04:21:41.493Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/launchapp-dev/animus-cli","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/launchapp-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-17T20:49:40.000Z","updated_at":"2026-05-31T02:14:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/launchapp-dev/animus-protocol","commit_stats":null,"previous_names":["launchapp-dev/animus-protocol"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/launchapp-dev/animus-protocol","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchapp-dev%2Fanimus-protocol","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchapp-dev%2Fanimus-protocol/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchapp-dev%2Fanimus-protocol/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchapp-dev%2Fanimus-protocol/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/launchapp-dev","download_url":"https://codeload.github.com/launchapp-dev/animus-protocol/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/launchapp-dev%2Fanimus-protocol/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34029790,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-07T02:00:07.652Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-18T00:10:03.458Z","updated_at":"2026-06-07T17:00:23.188Z","avatar_url":"https://github.com/launchapp-dev.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# animus-protocol\n\n**The plugin protocol stack for [Animus](https://github.com/launchapp-dev/animus-cli).** Build a subject backend, an LLM provider, or any custom Animus plugin in Rust — or in any language that can speak newline-delimited JSON-RPC 2.0 over stdio.\n\n## Status\n\n**v0.1.0 scaffold — protocol design + wire types + spec landed; runtime helpers standalone-compilable.** Animus core v0.4.0 ships these crates as workspace members (`crates/animus-{plugin,subject,provider}-protocol/` + `crates/animus-plugin-runtime/` in [`launchapp-dev/animus-cli`](https://github.com/launchapp-dev/animus-cli)), so the design + wire types have been exercised against a real codebase. The standalone repo now compiles cleanly end-to-end. Honest state before the first `cargo publish` to crates.io:\n\n| Crate | Standalone-compilable? | Notes |\n|---|---|---|\n| `animus-plugin-protocol` | yes | Wire types only; no external Animus deps. |\n| `animus-subject-protocol` | yes | Pure trait + schema definitions. |\n| `animus-provider-protocol` | yes | Pure trait + schema definitions. |\n| `animus-trigger-protocol` | yes | Pure trait + schema definitions for push-driven event sources (Slack, webhooks, file watchers, cron). |\n| `animus-log-storage-protocol` | yes | Pure trait + schema definitions for log storage backends (local `events.jsonl` file, Loki, Splunk, ClickHouse). |\n| `animus-plugin-runtime` | yes | Slim stdio JSON-RPC loop; exposes `subject_backend_main`, `provider_main`, `trigger_backend_main`, and `log_storage_backend_main`. Provider session helpers (event channels, child-process plumbing) will land in a separate `animus-session-backend` crate. |\n\nThe protocol [`spec.md`](./spec.md) is the source of truth for cross-language plugin authors — it can be implemented in Python, TypeScript, Go, or any language that speaks newline-delimited JSON-RPC 2.0 over stdio.\n\nThe protocol + subject + provider + runtime crates are usable today via git path/tag dependency from this repo. Plugin authors write `subject_backend_main(info, backend).await` or `provider_main(info, backend).await` from `main` and avoid hand-rolling the wire layer.\n\n## Crates\n\n| Crate | Purpose |\n|---|---|\n| [`animus-plugin-protocol`](./animus-plugin-protocol) | Wire types every plugin uses: `RpcRequest`, `RpcResponse`, `RpcNotification`, `RpcError`, error codes, `InitializeParams` / `InitializeResult`, `PluginManifest`, `HealthCheckResult`. |\n| [`animus-subject-protocol`](./animus-subject-protocol) | `SubjectBackend` trait + normalized `Subject` schema for backends like Linear, Jira, GitHub Issues, Notion, Asana — anything with a system-of-record API. |\n| [`animus-provider-protocol`](./animus-provider-protocol) | `ProviderBackend` trait + `AgentRunRequest`/`AgentRunResponse` shapes for LLM provider plugins (Claude, Codex, Gemini, OpenAI-compatible, on-prem). |\n| [`animus-trigger-protocol`](./animus-trigger-protocol) | `TriggerBackend` trait + `TriggerEvent`/`TriggerSchema` shapes for push-driven event sources (Slack mentions, generic webhooks, file watchers, cron). |\n| [`animus-log-storage-protocol`](./animus-log-storage-protocol) | `LogStorageBackend` trait + `LogEntry`/`LogQuery`/`LogQueryResult`/`LogStorageSchema` shapes for log storage backends (local `events.jsonl` file, Loki, Splunk, ClickHouse). |\n| [`animus-plugin-runtime`](./animus-plugin-runtime) | Shared stdio JSON-RPC loop, handshake, `--manifest` mode, notification helpers. Plugin authors call `subject_backend_main(...)` / `provider_main(...)` / `trigger_backend_main(...)` / `log_storage_backend_main(...)` from `main` and avoid hand-rolling the wire layer. |\n\n`animus-plugin-protocol` is the only required dependency for non-Rust plugin authors — and even then only as a reference. Any process that emits the documented JSON over stdio is a compatible Animus plugin.\n\n## Subject backend quickstart (Rust)\n\nCargo.toml:\n\n```toml\n[package]\nname = \"animus-subject-linear\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nanimus-plugin-protocol = \"0.1\"\nanimus-subject-protocol = \"0.1\"\nanimus-plugin-runtime   = \"0.1\"\nasync-trait = \"0.1\"\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\"] }\n```\n\nsrc/main.rs:\n\n```rust\nuse animus_plugin_protocol::{PluginInfo, PLUGIN_KIND_SUBJECT_BACKEND};\nuse animus_plugin_runtime::subject_backend_main;\n\nmod backend;\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let info = PluginInfo {\n        name: \"animus-subject-linear\".into(),\n        version: env!(\"CARGO_PKG_VERSION\").into(),\n        plugin_kind: PLUGIN_KIND_SUBJECT_BACKEND.into(),\n        description: Some(\"Linear subject backend for Animus\".into()),\n    };\n    subject_backend_main(info, backend::LinearBackend::new()).await\n}\n```\n\nsrc/backend.rs (sketch):\n\n```rust\nuse animus_plugin_protocol::{HealthCheckResult, HealthStatus};\nuse animus_subject_protocol::{\n    BackendError, EventStream, StatusDispatchHint, Subject, SubjectAttachment, SubjectBackend,\n    SubjectFilter, SubjectId, SubjectList, SubjectPatch, SubjectSchema, SubjectStatus,\n};\nuse async_trait::async_trait;\nuse chrono::Utc;\n\npub struct LinearBackend { /* api client, etc. */ }\n\nimpl LinearBackend {\n    pub fn new() -\u003e Self { Self { /* ... */ } }\n}\n\n#[async_trait]\nimpl SubjectBackend for LinearBackend {\n    async fn list(\u0026self, _filter: SubjectFilter) -\u003e Result\u003cSubjectList, BackendError\u003e {\n        // Call Linear's API, map to Subject, return.\n        // A richer Subject surfaces the native state vocabulary verbatim and\n        // attaches any documents the issue carries so workflows can dispatch\n        // on `native_status`, `dispatch_label`, or attachment presence.\n        let subject = Subject {\n            id: SubjectId::new(\"linear:ENG-123\"),\n            kind: \"issue\".into(),\n            title: \"Implement subject backend protocol\".into(),\n            description: None,\n            status: SubjectStatus::InProgress,\n            priority: Some(3),\n            assignee: Some(\"agent:default\".into()),\n            labels: vec![\"backend\".into()],\n            parent: None,\n            children: vec![],\n            url: Some(\"https://linear.app/...\".into()),\n            created_at: Utc::now(),\n            updated_at: Utc::now(),\n            custom: Default::default(),\n            native_status: Some(\"In Review\".into()),\n            status_metadata: serde_json::json!({ \"state_id\": \"abc\", \"color\": \"#FFAA00\" }),\n            attachments: vec![SubjectAttachment {\n                id: \"doc-1\".into(),\n                kind: \"document\".into(),\n                uri: \"linear://issue/ENG-123/doc/spec\".into(),\n                title: Some(\"Spec\".into()),\n                mime_type: Some(\"text/markdown\".into()),\n                metadata: serde_json::Value::Null,\n            }],\n        };\n        Ok(SubjectList { subjects: vec![subject], next_cursor: None, fetched_at: Utc::now() })\n    }\n\n    async fn get(\u0026self, id: \u0026SubjectId) -\u003e Result\u003cSubject, BackendError\u003e {\n        Err(BackendError::NotFound(id.to_string()))\n    }\n\n    async fn update(\u0026self, _id: \u0026SubjectId, _patch: SubjectPatch) -\u003e Result\u003cSubject, BackendError\u003e {\n        // Translate patch into a Linear mutation; return the refreshed Subject.\n        unimplemented!()\n    }\n\n    async fn watch(\u0026self) -\u003e Option\u003cEventStream\u003e { None }\n\n    fn schema(\u0026self) -\u003e SubjectSchema {\n        SubjectSchema {\n            kinds: vec![\"issue\".into()],\n            status_values: vec![\n                SubjectStatus::Ready,\n                SubjectStatus::InProgress,\n                SubjectStatus::Done,\n            ],\n            supports_watch: false,\n            supports_create: false,\n            supports_pagination: true,\n            native_status_values: vec![\n                \"Backlog\".into(),\n                \"Todo\".into(),\n                \"In Review\".into(),\n                \"Shipped\".into(),\n            ],\n            status_dispatch_hints: vec![\n                StatusDispatchHint {\n                    native_status: \"In Review\".into(),\n                    maps_to: SubjectStatus::InProgress,\n                    dispatch_label: Some(\"code-review\".into()),\n                    description: Some(\"Awaiting peer review\".into()),\n                },\n                StatusDispatchHint {\n                    native_status: \"Shipped\".into(),\n                    maps_to: SubjectStatus::Done,\n                    dispatch_label: Some(\"post-ship-qa\".into()),\n                    description: None,\n                },\n            ],\n            custom_fields: vec![],\n        }\n    }\n\n    async fn health(\u0026self) -\u003e Result\u003cHealthCheckResult, BackendError\u003e {\n        Ok(HealthCheckResult {\n            status: HealthStatus::Healthy,\n            uptime_ms: None,\n            memory_usage_bytes: None,\n            last_error: None,\n        })\n    }\n}\n```\n\nThe `native_status`, `status_metadata`, `attachments`, and `status_dispatch_hints` fields are v0.1.1 additions. Backends that don't yet surface them can omit them entirely — the wire output stays byte-identical to v0.1.0. Workflow YAML in newer hosts can then gate phases on `dispatch_label` to fire phases like `code-review` regardless of which backend's vocabulary the subject came from. See [`spec.md` §9.7](./spec.md).\n\nRun:\n\n```bash\ncargo build --release\nanimus plugin install ./target/release/animus-subject-linear\n```\n\n## Provider quickstart (Rust)\n\n```rust\nuse animus_plugin_protocol::{PluginInfo, PLUGIN_KIND_PROVIDER};\nuse animus_plugin_runtime::provider_main;\n\nmod provider;\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let info = PluginInfo {\n        name: \"animus-provider-claude\".into(),\n        version: env!(\"CARGO_PKG_VERSION\").into(),\n        plugin_kind: PLUGIN_KIND_PROVIDER.into(),\n        description: Some(\"Claude Code CLI provider\".into()),\n    };\n    provider_main(info, provider::ClaudeProvider::new()).await\n}\n```\n\n`provider::ClaudeProvider` implements [`ProviderBackend`](./animus-provider-protocol) — `manifest`, `run_agent`, `resume_agent`, `cancel_agent`, and `health`. The runtime handles `initialize`, `$/ping`, `health/check`, `agent/run`, `agent/resume`, `agent/cancel`, and `shutdown`, and dispatches each call into the trait implementation.\n\nFor v0.1.0 each `run_agent` call is request/response: the provider runs the session to completion inside the trait method and returns the aggregated [`AgentRunResponse`](./animus-provider-protocol). A streaming event-emitter API (so the runtime can flush `agent/output` / `agent/thinking` / `agent/toolCall` / `agent/toolResult` / `agent/error` notifications mid-run) will land in a follow-up `animus-session-backend` crate.\n\n## Trigger backend quickstart (Rust)\n\n```rust\nuse animus_plugin_protocol::{PluginInfo, PLUGIN_KIND_TRIGGER_BACKEND};\nuse animus_plugin_runtime::trigger_backend_main;\n\nmod backend;\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let info = PluginInfo {\n        name: \"animus-trigger-slack\".into(),\n        version: env!(\"CARGO_PKG_VERSION\").into(),\n        plugin_kind: PLUGIN_KIND_TRIGGER_BACKEND.into(),\n        description: Some(\"Slack trigger backend for Animus\".into()),\n    };\n    trigger_backend_main(info, backend::SlackBackend::new()).await\n}\n```\n\nsrc/backend.rs (sketch):\n\n```rust\nuse animus_plugin_protocol::{HealthCheckResult, HealthStatus};\nuse animus_trigger_protocol::{\n    BackendError, TriggerBackend, TriggerEvent, TriggerSchema, TriggerStream,\n};\nuse async_trait::async_trait;\nuse chrono::Utc;\nuse futures_core::stream;\n\npub struct SlackBackend { /* socket-mode client, etc. */ }\n\nimpl SlackBackend {\n    pub fn new() -\u003e Self { Self { /* ... */ } }\n}\n\n#[async_trait]\nimpl TriggerBackend for SlackBackend {\n    fn schema(\u0026self) -\u003e TriggerSchema {\n        TriggerSchema {\n            kinds: vec![\"slack_mention\".into(), \"slack_channel_message\".into()],\n            supports_resume: true,\n            supports_dedup: true,\n            supports_ack: true,\n        }\n    }\n\n    async fn watch(\u0026self) -\u003e Result\u003cTriggerStream, BackendError\u003e {\n        // In a real backend you'd subscribe to Slack socket-mode here and\n        // yield each event as a `TriggerEvent`. This sketch emits one\n        // synthetic event and ends.\n        let event = TriggerEvent {\n            id: \"slack:T123/C456/1715701234.000100\".into(),\n            occurred_at: Utc::now(),\n            kind: \"slack_mention\".into(),\n            payload: serde_json::json!({\"user\": \"U1\", \"text\": \"@animus please review\"}),\n            subject_id: None,\n            action_hint: Some(\"run-workflow:review\".into()),\n        };\n        Ok(Box::pin(stream::iter(vec![Ok(event)])))\n    }\n\n    async fn ack(\u0026self, _event_id: \u0026str) -\u003e Result\u003c(), BackendError\u003e {\n        // Persist the cursor so we don't redeliver after restart.\n        Ok(())\n    }\n\n    async fn health(\u0026self) -\u003e Result\u003cHealthCheckResult, BackendError\u003e {\n        Ok(HealthCheckResult {\n            status: HealthStatus::Healthy,\n            uptime_ms: None,\n            memory_usage_bytes: None,\n            last_error: None,\n        })\n    }\n}\n```\n\nThe runtime calls `watch` once after `initialize`/`initialized`, forwards every event the stream yields as a `trigger/event` notification, and dispatches `trigger/ack` calls back into the trait. Backends that don't track delivery state can rely on the default no-op `ack` implementation.\n\n## Log storage backend quickstart (Rust)\n\n```rust\nuse animus_log_storage_protocol::PLUGIN_KIND_LOG_STORAGE_BACKEND;\nuse animus_plugin_protocol::PluginInfo;\nuse animus_plugin_runtime::log_storage_backend_main;\n\nmod backend;\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let info = PluginInfo {\n        name: \"animus-log-storage-file\".into(),\n        version: env!(\"CARGO_PKG_VERSION\").into(),\n        plugin_kind: PLUGIN_KIND_LOG_STORAGE_BACKEND.into(),\n        description: Some(\"Local events.jsonl log storage\".into()),\n    };\n    log_storage_backend_main(info, backend::FileBackend::new()).await\n}\n```\n\nsrc/backend.rs (sketch):\n\n```rust\nuse animus_log_storage_protocol::{\n    BackendError, LogEntry, LogQuery, LogQueryResult, LogStorageBackend, LogStorageSchema,\n    LogStream, SupportsFiltering,\n};\nuse animus_plugin_protocol::{HealthCheckResult, HealthStatus};\nuse async_trait::async_trait;\nuse futures_core::stream;\n\npub struct FileBackend { /* file handle, mutex, ... */ }\n\nimpl FileBackend {\n    pub fn new() -\u003e Self { Self { /* ... */ } }\n}\n\n#[async_trait]\nimpl LogStorageBackend for FileBackend {\n    async fn store(\u0026self, _entries: Vec\u003cLogEntry\u003e) -\u003e Result\u003c(), BackendError\u003e {\n        // Append each entry as one JSON line to events.jsonl. Dedup by entry.id\n        // if the file already contains it (or skip dedup and rely on the\n        // host).\n        Ok(())\n    }\n\n    async fn query(\u0026self, _filter: LogQuery) -\u003e Result\u003cLogQueryResult, BackendError\u003e {\n        // Scan events.jsonl, filter in-process, return.\n        Ok(LogQueryResult { entries: vec![], next_cursor: None })\n    }\n\n    async fn tail(\u0026self, _filter: LogQuery) -\u003e Result\u003cLogStream, BackendError\u003e {\n        // Open a follower over the JSONL file (inotify, kqueue, polling, ...)\n        // and yield each new entry as it lands.\n        Ok(Box::pin(stream::iter(Vec::\u003cResult\u003cLogEntry, BackendError\u003e\u003e::new())))\n    }\n\n    fn schema(\u0026self) -\u003e LogStorageSchema {\n        LogStorageSchema {\n            supports_query: true,\n            supports_tail: true,\n            supports_dedup: false,\n            supports_filtering: SupportsFiltering {\n                by_level: true,\n                by_source: true,\n                by_target: true,\n                by_time_range: true,\n                by_glob: true,\n            },\n            max_query_window: None,\n            retention_hint: None,\n        }\n    }\n\n    async fn health(\u0026self) -\u003e Result\u003cHealthCheckResult, BackendError\u003e {\n        Ok(HealthCheckResult {\n            status: HealthStatus::Healthy,\n            uptime_ms: None,\n            memory_usage_bytes: None,\n            last_error: None,\n        })\n    }\n}\n```\n\nThe runtime handles `initialize`, `$/ping`, `health/check`, `log_storage/store`, `log_storage/query`, `log_storage/tail` (with `log_storage/event` notification streaming), `log_storage/schema`, and `shutdown`, and dispatches each call into the trait implementation. Write-only sinks set `supports_query = false` / `supports_tail = false` in the schema and return `BackendError::NotSupported` from the corresponding methods — the runtime translates that to `-32001` (`method_not_supported`) on the wire and hosts fall back gracefully.\n\n## Source of truth\n\n- [`spec.md`](./spec.md) is the language-agnostic protocol specification. **A Python or TypeScript plugin that conforms to `spec.md` is a first-class Animus plugin.** The Rust crates in this repo are one reference implementation.\n- [`animus-plugin-protocol/src/lib.rs`](./animus-plugin-protocol/src/lib.rs) is the canonical type definitions; the spec mirrors them.\n\n## Design pointers\n\nThe plugin protocol exists because of two design constraints:\n\n- **No system-of-record migration.** Most teams will not move work out of Linear / Jira / GitHub Issues. See [Subject Backend Plugins](https://github.com/launchapp-dev/animus-cli/blob/main/docs/architecture/subject-backend-plugins.md).\n- **Provider parity.** Claude, Codex, Gemini, and any future LLM CLI should plug into the daemon through the same surface as a custom HTTP provider. See [Naming Contract](https://github.com/launchapp-dev/animus-cli/blob/main/docs/architecture/naming-contract.md) and [Subject Dispatch Daemon](https://github.com/launchapp-dev/animus-cli/blob/main/docs/architecture/subject-dispatch-daemon.md).\n\n## License\n\nMIT. Copyright (c) 2026 Launchapp.dev. See [`LICENSE`](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchapp-dev%2Fanimus-protocol","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flaunchapp-dev%2Fanimus-protocol","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchapp-dev%2Fanimus-protocol/lists"}