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}