Skip to main content

ijima_core/
harness.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4//! Typed identifiers for the agentic harnesses that connect to Ijima.
5//!
6//! Every memory and session is tagged with its originating harness so
7//! that provenance is preserved end-to-end. Using an exhaustive enum
8//! rather than a raw `&str` lets the compiler flag callers when a new
9//! harness is added.
10
11/// The set of harnesses known to write to or read from Ijima.
12///
13/// This is intentionally exhaustive: adding a harness is a one-line
14/// change that the compiler propagates to every `match` site.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub enum Harness {
18    /// Dominic — meta-orchestrator.
19    Dominic,
20    /// Wallace — multi-user TUI.
21    Wallace,
22    /// Sakamoto — pipeline coding.
23    Sakamoto,
24    /// Tsume — gateway adapter (Discord, etc.).
25    Tsume,
26    /// pi — deep-work coding sessions.
27    Pi,
28    /// opencode — coding CLI.
29    Opencode,
30    /// Proserpina — critique pipeline (future).
31    Proserpina,
32    /// Any other harness not yet promoted to a dedicated variant.
33    Other,
34}
35
36impl Harness {
37    /// Returns the lowercase wire string used in stored records and the
38    /// HTTP API. Stable across releases; do not rename without a
39    /// migration.
40    pub fn as_wire_str(&self) -> &'static str {
41        match self {
42            Harness::Dominic => "dominic",
43            Harness::Wallace => "wallace",
44            Harness::Sakamoto => "sakamoto",
45            Harness::Tsume => "tsume",
46            Harness::Pi => "pi",
47            Harness::Opencode => "opencode",
48            Harness::Proserpina => "proserpina",
49            Harness::Other => "other",
50        }
51    }
52
53    /// Parses a wire string back into a [`Harness`]. Unknown strings
54    /// map to [`Harness::Other`] so records from a future harness don't
55    /// break deserialization.
56    pub fn from_wire_str(s: &str) -> Self {
57        match s {
58            "dominic" => Harness::Dominic,
59            "wallace" => Harness::Wallace,
60            "sakamoto" => Harness::Sakamoto,
61            "tsume" => Harness::Tsume,
62            "pi" => Harness::Pi,
63            "opencode" => Harness::Opencode,
64            "proserpina" => Harness::Proserpina,
65            _ => Harness::Other,
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn wire_strings_are_stable_and_lowercase() {
76        assert_eq!(Harness::Pi.as_wire_str(), "pi");
77        assert_eq!(Harness::Tsume.as_wire_str(), "tsume");
78        for h in [
79            Harness::Dominic,
80            Harness::Wallace,
81            Harness::Sakamoto,
82            Harness::Tsume,
83            Harness::Pi,
84            Harness::Opencode,
85            Harness::Proserpina,
86            Harness::Other,
87        ] {
88            let s = h.as_wire_str();
89            assert!(
90                s == s.to_ascii_lowercase(),
91                "wire string for {h:?} must be lowercase"
92            );
93        }
94    }
95
96    #[test]
97    fn from_wire_str_round_trips_known_harnesses() {
98        for h in [
99            Harness::Dominic,
100            Harness::Wallace,
101            Harness::Sakamoto,
102            Harness::Tsume,
103            Harness::Pi,
104            Harness::Opencode,
105            Harness::Proserpina,
106        ] {
107            assert_eq!(Harness::from_wire_str(h.as_wire_str()), h);
108        }
109        // Unknown strings fall back to Other (forward-compat).
110        assert_eq!(Harness::from_wire_str("future-harness"), Harness::Other);
111    }
112}