refactor: clear pi-era residue + make CI gate green (harness-neutral cleanup) #158
12 changed files with 121 additions and 76 deletions
12
AGENTS.md
12
AGENTS.md
|
|
@ -102,6 +102,18 @@ cargo test --workspace
|
||||||
cargo build --workspace --release
|
cargo build --workspace --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Mandatory before every merge to `main`:** run the full gate via
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/ci-checks.sh # fmt --check, clippy -D warnings, test, markdown gate
|
||||||
|
```
|
||||||
|
|
||||||
|
`.forgejo/workflows/ci.yml` encodes the same checks, but **no Actions runner is
|
||||||
|
currently registered**, so nothing enforces them server-side. Until a runner is
|
||||||
|
active, `ci-checks.sh` passing locally is the only gate — a green run is a
|
||||||
|
prerequisite for merging. (A compile break reached `main` once because this step
|
||||||
|
was skipped; do not skip it.)
|
||||||
|
|
||||||
## ISO Takeover Gates
|
## ISO Takeover Gates
|
||||||
|
|
||||||
| Gate | Target | Status |
|
| Gate | Target | Status |
|
||||||
|
|
|
||||||
|
|
@ -327,7 +327,7 @@ mod tests {
|
||||||
"id": "pane-1",
|
"id": "pane-1",
|
||||||
"agent": "pi",
|
"agent": "pi",
|
||||||
"state": "working",
|
"state": "working",
|
||||||
"pi_session_id": "pi-session-1",
|
"session_id": "pi-session-1",
|
||||||
"last_event_at": "2026-05-27T11:59:59.000Z",
|
"last_event_at": "2026-05-27T11:59:59.000Z",
|
||||||
"cwd": "/repo"
|
"cwd": "/repo"
|
||||||
}]
|
}]
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ async fn daemon_client_live_socket_check_with_local_fake_agent() {
|
||||||
.iter()
|
.iter()
|
||||||
.find(|pane| pane.id == agent_id)
|
.find(|pane| pane.id == agent_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(pane.pi_session_id.as_deref(), Some("manual-test"));
|
assert_eq!(pane.session_id.as_deref(), Some("manual-test"));
|
||||||
assert!(pane.cwd.is_some());
|
assert!(pane.cwd.is_some());
|
||||||
|
|
||||||
let kill = client.kill_agent(agent_id).await.unwrap();
|
let kill = client.kill_agent(agent_id).await.unwrap();
|
||||||
|
|
@ -370,7 +370,7 @@ async fn harness_double_spawn_session_isolation() {
|
||||||
assert_ne!(pane_a.id, pane_b.id);
|
assert_ne!(pane_a.id, pane_b.id);
|
||||||
|
|
||||||
// Verify session isolation: both share the same session_id (test agent default)
|
// Verify session isolation: both share the same session_id (test agent default)
|
||||||
assert_eq!(pane_a.pi_session_id, pane_b.pi_session_id);
|
assert_eq!(pane_a.session_id, pane_b.session_id);
|
||||||
|
|
||||||
// Kill one agent — snapshot may still include stopped panes briefly
|
// Kill one agent — snapshot may still include stopped panes briefly
|
||||||
let kill = client.kill_agent(&pane_a.id).await.unwrap();
|
let kill = client.kill_agent(&pane_a.id).await.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ pub struct RuntimeInventory {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub pi: Option<String>,
|
pub pi: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub zot: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub npm_prefix: Option<String>,
|
pub npm_prefix: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub package_manager: Option<String>,
|
pub package_manager: Option<String>,
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let socket_handle =
|
let socket_handle =
|
||||||
tokio::spawn(async move { socket::serve(socket_state, socket_shutdown).await });
|
tokio::spawn(async move { socket::serve(socket_state, socket_shutdown).await });
|
||||||
|
|
||||||
// Auto-spawn one Pi agent if configured (live "Operator Image" OOTB flow).
|
// Auto-spawn one agent if configured (live "Operator Image" OOTB flow).
|
||||||
// Runs after the socket server is up so the spawn registers on the board;
|
// Runs after the socket server is up so the spawn registers on the board;
|
||||||
// no-op unless COLIBRI_AUTOSPAWN is set and a DeepSeek key is present.
|
// no-op unless COLIBRI_AUTOSPAWN is set and a DeepSeek key is present.
|
||||||
socket::autospawn_agent_if_configured(&state).await;
|
socket::autospawn_agent_if_configured(&state).await;
|
||||||
|
|
|
||||||
|
|
@ -416,9 +416,7 @@ pub async fn autospawn_agent_if_configured(state: &SharedState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if state.config.deepseek_api_key.is_none() {
|
if state.config.deepseek_api_key.is_none() {
|
||||||
info!(
|
info!("autospawn: DEEPSEEK_API_KEY not set; skipping (operator can add it via Join Hive)");
|
||||||
"autospawn: DEEPSEEK_API_KEY not set; skipping (operator can add it via Join Hive)"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -438,22 +436,13 @@ pub async fn autospawn_agent_if_configured(state: &SharedState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default argv depends on the harness: zot is a request/response peer
|
// Default argv depends on the harness (see default_agent_args). Override
|
||||||
// driven over stdin (`zot rpc`), while pi self-drives (`pi --mode json`).
|
// wholesale with COLIBRI_AUTOSPAWN_ARGS.
|
||||||
// `--mode json` is NOT a valid zot flag, so a single default would break one
|
|
||||||
// of them — pick per binary. Override with COLIBRI_AUTOSPAWN_ARGS.
|
|
||||||
let default_args = if agent_name == "zot" {
|
|
||||||
"rpc"
|
|
||||||
} else {
|
|
||||||
"--mode json"
|
|
||||||
};
|
|
||||||
let args: Vec<String> = std::env::var("COLIBRI_AUTOSPAWN_ARGS")
|
let args: Vec<String> = std::env::var("COLIBRI_AUTOSPAWN_ARGS")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s| !s.trim().is_empty())
|
.filter(|s| !s.trim().is_empty())
|
||||||
.unwrap_or_else(|| default_args.to_string())
|
.map(|s| s.split_whitespace().map(str::to_string).collect())
|
||||||
.split_whitespace()
|
.unwrap_or_else(|| default_agent_args(&agent_binary));
|
||||||
.map(str::to_string)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
info!(binary = %agent_binary, ?args, "autospawn: spawning agent on host (DeepSeek-backed)");
|
info!(binary = %agent_binary, ?args, "autospawn: spawning agent on host (DeepSeek-backed)");
|
||||||
|
|
||||||
|
|
@ -554,6 +543,20 @@ fn basename(path: &str) -> String {
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default argv for an agent harness, by binary basename. zot is an RPC peer
|
||||||
|
/// (`zot rpc`, driven over stdin); every other harness is assumed to be a
|
||||||
|
/// self-driving JSON emitter (`pi --mode json`). `--mode json` is not a valid
|
||||||
|
/// zot flag, so the default must vary by harness — a single shared default
|
||||||
|
/// would break one of them. Single source of truth for both autospawn and the
|
||||||
|
/// non-local spawn path.
|
||||||
|
fn default_agent_args(binary: &str) -> Vec<String> {
|
||||||
|
if basename(binary) == "zot" {
|
||||||
|
vec!["rpc".to_string()]
|
||||||
|
} else {
|
||||||
|
vec!["--mode".to_string(), "json".to_string()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn env_truthy(name: &str) -> bool {
|
fn env_truthy(name: &str) -> bool {
|
||||||
std::env::var(name)
|
std::env::var(name)
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
|
|
@ -565,6 +568,10 @@ fn env_truthy(name: &str) -> bool {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn parameters are passed positionally to mirror the socket command shape;
|
||||||
|
// grouping them into a struct would not improve clarity here. Matches the same
|
||||||
|
// allow on prepare_spawn_command.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn cmd_spawn_agent(
|
async fn cmd_spawn_agent(
|
||||||
state: &SharedState,
|
state: &SharedState,
|
||||||
provider_str: String,
|
provider_str: String,
|
||||||
|
|
@ -590,10 +597,12 @@ async fn cmd_spawn_agent(
|
||||||
let args = local_args.unwrap_or_default();
|
let args = local_args.unwrap_or_default();
|
||||||
(model.clone(), args)
|
(model.clone(), args)
|
||||||
} else {
|
} else {
|
||||||
(
|
// Non-local provider: a managed agent harness. Default to zot (the
|
||||||
std::env::var("COLIBRI_AGENT_BINARY").unwrap_or_else(|_| "hermes-agent".to_string()),
|
// current default harness); argv follows the harness via
|
||||||
vec!["--mode".to_string(), "json".to_string()],
|
// default_agent_args so we never spawn `zot --mode json`.
|
||||||
)
|
let binary = std::env::var("COLIBRI_AGENT_BINARY").unwrap_or_else(|_| "zot".to_string());
|
||||||
|
let args = default_agent_args(&binary);
|
||||||
|
(binary, args)
|
||||||
};
|
};
|
||||||
|
|
||||||
// An agent invoked in RPC mode (`zot rpc` / `--rpc`) blocks on stdin until
|
// An agent invoked in RPC mode (`zot rpc` / `--rpc`) blocks on stdin until
|
||||||
|
|
@ -1093,7 +1102,7 @@ mod tests {
|
||||||
let data = response.data.unwrap();
|
let data = response.data.unwrap();
|
||||||
assert_eq!(data["schema"], "clawdie.glasspane.snapshot.v1");
|
assert_eq!(data["schema"], "clawdie.glasspane.snapshot.v1");
|
||||||
assert_eq!(data["panes"][0]["id"], "pane-a");
|
assert_eq!(data["panes"][0]["id"], "pane-a");
|
||||||
assert_eq!(data["panes"][0]["pi_session_id"], "pi-s");
|
assert_eq!(data["panes"][0]["session_id"], "pi-s");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -1130,7 +1139,7 @@ mod tests {
|
||||||
.find(|pane| pane.id == pane_id)
|
.find(|pane| pane.id == pane_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(pane.state, colibri_glasspane::AgentState::Done);
|
assert_eq!(pane.state, colibri_glasspane::AgentState::Done);
|
||||||
assert_eq!(pane.pi_session_id.as_deref(), Some("pi-fake"));
|
assert_eq!(pane.session_id.as_deref(), Some("pi-fake"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
|
|
@ -317,11 +317,7 @@ impl RpcSender {
|
||||||
/// Send one prompt as a newline-delimited request on the agent's stdin.
|
/// Send one prompt as a newline-delimited request on the agent's stdin.
|
||||||
/// Returns the request id used. Errors if the stdin pipe has closed.
|
/// Returns the request id used. Errors if the stdin pipe has closed.
|
||||||
pub async fn send_prompt(&self, message: &str) -> std::io::Result<String> {
|
pub async fn send_prompt(&self, message: &str) -> std::io::Result<String> {
|
||||||
let id = (self
|
let id = (self.seq.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1).to_string();
|
||||||
.seq
|
|
||||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
|
|
||||||
+ 1)
|
|
||||||
.to_string();
|
|
||||||
let line = build_rpc_prompt(&id, message);
|
let line = build_rpc_prompt(&id, message);
|
||||||
let mut guard = self.stdin.lock().await;
|
let mut guard = self.stdin.lock().await;
|
||||||
let stdin = guard.as_mut().ok_or_else(|| {
|
let stdin = guard.as_mut().ok_or_else(|| {
|
||||||
|
|
@ -552,7 +548,7 @@ pub fn jail_wrap(
|
||||||
/// Configuration for spawning an agent subprocess.
|
/// Configuration for spawning an agent subprocess.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AgentSpawnConfig {
|
pub struct AgentSpawnConfig {
|
||||||
/// Agent binary or command to run (e.g. "pi", "hermes-agent", etc.).
|
/// Agent binary or command to run (e.g. "zot", "pi", etc.).
|
||||||
pub binary: String,
|
pub binary: String,
|
||||||
/// Command-line arguments.
|
/// Command-line arguments.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ mod integration {
|
||||||
ingestor.ingest_line_at(line, t + Duration::from_secs(i as u64));
|
ingestor.ingest_line_at(line, t + Duration::from_secs(i as u64));
|
||||||
}
|
}
|
||||||
assert_eq!(ingestor.state(), AgentState::Done);
|
assert_eq!(ingestor.state(), AgentState::Done);
|
||||||
assert_eq!(ingestor.pi_session_id(), Some("s1"));
|
assert_eq!(ingestor.session_id(), Some("s1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Agent stays idle through unknown events (no crash, forward-compatible)
|
/// Agent stays idle through unknown events (no crash, forward-compatible)
|
||||||
|
|
@ -81,7 +81,7 @@ mod integration {
|
||||||
id: "a".into(),
|
id: "a".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Working,
|
state: AgentState::Working,
|
||||||
pi_session_id: Some("s1".into()),
|
session_id: Some("s1".into()),
|
||||||
last_event_at: Some("2026-05-27T10:00:00Z".into()),
|
last_event_at: Some("2026-05-27T10:00:00Z".into()),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -90,7 +90,7 @@ mod integration {
|
||||||
id: "b".into(),
|
id: "b".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Blocked,
|
state: AgentState::Blocked,
|
||||||
pi_session_id: Some("s2".into()),
|
session_id: Some("s2".into()),
|
||||||
last_event_at: Some("2026-05-27T10:00:01Z".into()),
|
last_event_at: Some("2026-05-27T10:00:01Z".into()),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -99,7 +99,7 @@ mod integration {
|
||||||
id: "c".into(),
|
id: "c".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Idle,
|
state: AgentState::Idle,
|
||||||
pi_session_id: None,
|
session_id: None,
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -124,7 +124,7 @@ mod integration {
|
||||||
id: "p1".into(),
|
id: "p1".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Working,
|
state: AgentState::Working,
|
||||||
pi_session_id: Some("sess-1".into()),
|
session_id: Some("sess-1".into()),
|
||||||
last_event_at: Some("2026-05-27T10:00:00Z".into()),
|
last_event_at: Some("2026-05-27T10:00:00Z".into()),
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -141,7 +141,7 @@ mod integration {
|
||||||
fn default_ingestor_is_idle() {
|
fn default_ingestor_is_idle() {
|
||||||
let ingestor = PiJsonlIngestor::default();
|
let ingestor = PiJsonlIngestor::default();
|
||||||
assert_eq!(ingestor.state(), AgentState::Idle);
|
assert_eq!(ingestor.state(), AgentState::Idle);
|
||||||
assert!(ingestor.pi_session_id().is_none());
|
assert!(ingestor.session_id().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue update blocks agent
|
/// Queue update blocks agent
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! Uses scripts/fake-pi-agent.py which emits the colibri-pi-events JSONL taxonomy.
|
//! Uses scripts/fake-pi-agent.py which emits the colibri-pi-events JSONL taxonomy.
|
||||||
//!
|
//!
|
||||||
//! With real Pi binary (when installed):
|
//! With real Pi binary (when installed):
|
||||||
//! COLIBRI_PI_BINARY=pi cargo test -p colibri-daemon --test pi_spawn_live -- --nocapture
|
//! COLIBRI_AGENT_BINARY=pi cargo test -p colibri-daemon --test pi_spawn_live -- --nocapture
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
|
@ -79,13 +79,13 @@ async fn pi_spawn_path_produces_correct_glasspane_state() {
|
||||||
"expected Done after full agent run"
|
"expected Done after full agent run"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
pane.pi_session_id.is_some(),
|
pane.session_id.is_some(),
|
||||||
"expected pi_session_id from session event, got {:?}",
|
"expected session_id from session event, got {:?}",
|
||||||
pane.pi_session_id
|
pane.session_id
|
||||||
);
|
);
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"✓ Pi spawn proof: {} accepted, state={:?}, session={:?}",
|
"✓ Pi spawn proof: {} accepted, state={:?}, session={:?}",
|
||||||
accepted, pane.state, pane.pi_session_id
|
accepted, pane.state, pane.session_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ struct App {
|
||||||
// harness additions
|
// harness additions
|
||||||
status_msg: Option<(String, u8)>, // (message, ttl ticks)
|
status_msg: Option<(String, u8)>, // (message, ttl ticks)
|
||||||
detail_pane: Option<usize>, // index into snapshot.panes, or None
|
detail_pane: Option<usize>, // index into snapshot.panes, or None
|
||||||
session_filter: Option<String>, // filter by pi_session_id, or None
|
session_filter: Option<String>, // filter by session_id, or None
|
||||||
sessions: Vec<String>, // all known session IDs
|
sessions: Vec<String>, // all known session IDs
|
||||||
session_idx: usize, // which session is selected
|
session_idx: usize, // which session is selected
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +113,7 @@ impl App {
|
||||||
Some(sid) => snap
|
Some(sid) => snap
|
||||||
.panes
|
.panes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|p| p.pi_session_id.as_deref() == Some(sid.as_str()))
|
.filter(|p| p.session_id.as_deref() == Some(sid.as_str()))
|
||||||
.collect(),
|
.collect(),
|
||||||
None => snap.panes.iter().collect(),
|
None => snap.panes.iter().collect(),
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +132,7 @@ impl App {
|
||||||
let mut ids: Vec<String> = snap
|
let mut ids: Vec<String> = snap
|
||||||
.panes
|
.panes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|p| p.pi_session_id.clone())
|
.filter_map(|p| p.session_id.clone())
|
||||||
.collect();
|
.collect();
|
||||||
ids.sort();
|
ids.sort();
|
||||||
ids.dedup();
|
ids.dedup();
|
||||||
|
|
@ -377,7 +377,7 @@ impl App {
|
||||||
format!("{:?}", p.state),
|
format!("{:?}", p.state),
|
||||||
Style::default().fg(color),
|
Style::default().fg(color),
|
||||||
)),
|
)),
|
||||||
Cell::from(p.pi_session_id.as_deref().unwrap_or("—")),
|
Cell::from(p.session_id.as_deref().unwrap_or("—")),
|
||||||
Cell::from(p.cwd.as_deref().unwrap_or("—")),
|
Cell::from(p.cwd.as_deref().unwrap_or("—")),
|
||||||
Cell::from(if p.stalled { "⚠" } else { "" }),
|
Cell::from(if p.stalled { "⚠" } else { "" }),
|
||||||
])
|
])
|
||||||
|
|
@ -429,7 +429,7 @@ impl App {
|
||||||
),
|
),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled("Session: ", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("Session: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(pane.pi_session_id.as_deref().unwrap_or("—")),
|
Span::raw(pane.session_id.as_deref().unwrap_or("—")),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("CWD: ", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("CWD: ", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
|
@ -649,7 +649,7 @@ mod tests {
|
||||||
id: "a".into(),
|
id: "a".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Working,
|
state: AgentState::Working,
|
||||||
pi_session_id: Some("s1".into()),
|
session_id: Some("s1".into()),
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -658,7 +658,7 @@ mod tests {
|
||||||
id: "b".into(),
|
id: "b".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Idle,
|
state: AgentState::Idle,
|
||||||
pi_session_id: Some("s2".into()),
|
session_id: Some("s2".into()),
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -693,7 +693,7 @@ mod tests {
|
||||||
id: "a".into(),
|
id: "a".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Working,
|
state: AgentState::Working,
|
||||||
pi_session_id: Some("s2".into()),
|
session_id: Some("s2".into()),
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -702,7 +702,7 @@ mod tests {
|
||||||
id: "b".into(),
|
id: "b".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Working,
|
state: AgentState::Working,
|
||||||
pi_session_id: Some("s1".into()),
|
session_id: Some("s1".into()),
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -711,7 +711,7 @@ mod tests {
|
||||||
id: "c".into(),
|
id: "c".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Working,
|
state: AgentState::Working,
|
||||||
pi_session_id: Some("s1".into()),
|
session_id: Some("s1".into()),
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ pub fn pane_from_jsonl_with_id(
|
||||||
id: pane_id.into(),
|
id: pane_id.into(),
|
||||||
agent: agent.into(),
|
agent: agent.into(),
|
||||||
state: ingestor.state,
|
state: ingestor.state,
|
||||||
pi_session_id: ingestor.pi_session_id,
|
session_id: ingestor.session_id,
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: ingestor.cwd,
|
cwd: ingestor.cwd,
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -223,7 +223,7 @@ pub fn pane_from_jsonl(agent: impl Into<String>, jsonl: &str) -> Pane {
|
||||||
pub struct PiStreamUpdate {
|
pub struct PiStreamUpdate {
|
||||||
pub pi_type: String,
|
pub pi_type: String,
|
||||||
pub state: AgentState,
|
pub state: AgentState,
|
||||||
pub pi_session_id: Option<String>,
|
pub session_id: Option<String>,
|
||||||
pub cwd: Option<String>,
|
pub cwd: Option<String>,
|
||||||
pub observed_at: SystemTime,
|
pub observed_at: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +244,7 @@ pub struct PaneReaderStats {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct PiJsonlIngestor {
|
pub struct PiJsonlIngestor {
|
||||||
state: AgentState,
|
state: AgentState,
|
||||||
pi_session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
cwd: Option<String>,
|
cwd: Option<String>,
|
||||||
last_event_at: Option<SystemTime>,
|
last_event_at: Option<SystemTime>,
|
||||||
/// Which harness produced the stream. Pi events are read directly; zot
|
/// Which harness produced the stream. Pi events are read directly; zot
|
||||||
|
|
@ -256,7 +256,7 @@ impl Default for PiJsonlIngestor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: AgentState::Idle,
|
state: AgentState::Idle,
|
||||||
pi_session_id: None,
|
session_id: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
runtime: AgentRuntime::Pi,
|
runtime: AgentRuntime::Pi,
|
||||||
|
|
@ -277,8 +277,8 @@ impl PiJsonlIngestor {
|
||||||
self.state
|
self.state
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pi_session_id(&self) -> Option<&str> {
|
pub fn session_id(&self) -> Option<&str> {
|
||||||
self.pi_session_id.as_deref()
|
self.session_id.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cwd(&self) -> Option<&str> {
|
pub fn cwd(&self) -> Option<&str> {
|
||||||
|
|
@ -312,7 +312,7 @@ impl PiJsonlIngestor {
|
||||||
|
|
||||||
if ty == "session" || ty == "session_started" {
|
if ty == "session" || ty == "session_started" {
|
||||||
if let Some(sid) = value.get("id").and_then(Value::as_str) {
|
if let Some(sid) = value.get("id").and_then(Value::as_str) {
|
||||||
self.pi_session_id = Some(sid.to_string());
|
self.session_id = Some(sid.to_string());
|
||||||
}
|
}
|
||||||
if let Some(cwd) = value.get("cwd").and_then(Value::as_str) {
|
if let Some(cwd) = value.get("cwd").and_then(Value::as_str) {
|
||||||
self.cwd = Some(cwd.to_string());
|
self.cwd = Some(cwd.to_string());
|
||||||
|
|
@ -322,7 +322,7 @@ impl PiJsonlIngestor {
|
||||||
Some(PiStreamUpdate {
|
Some(PiStreamUpdate {
|
||||||
pi_type: ty.to_string(),
|
pi_type: ty.to_string(),
|
||||||
state: self.state,
|
state: self.state,
|
||||||
pi_session_id: self.pi_session_id.clone(),
|
session_id: self.session_id.clone(),
|
||||||
cwd: self.cwd.clone(),
|
cwd: self.cwd.clone(),
|
||||||
observed_at,
|
observed_at,
|
||||||
})
|
})
|
||||||
|
|
@ -373,8 +373,8 @@ impl SupervisedPane {
|
||||||
self.ingestor.state()
|
self.ingestor.state()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pi_session_id(&self) -> Option<&str> {
|
pub fn session_id(&self) -> Option<&str> {
|
||||||
self.ingestor.pi_session_id()
|
self.ingestor.session_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cwd(&self) -> Option<&str> {
|
pub fn cwd(&self) -> Option<&str> {
|
||||||
|
|
@ -407,7 +407,7 @@ impl SupervisedPane {
|
||||||
id: self.id.clone(),
|
id: self.id.clone(),
|
||||||
agent: self.agent.clone(),
|
agent: self.agent.clone(),
|
||||||
state: self.state(),
|
state: self.state(),
|
||||||
pi_session_id: self.pi_session_id().map(str::to_string),
|
session_id: self.session_id().map(str::to_string),
|
||||||
last_event_at: self.last_event_at().map(system_time_to_rfc3339),
|
last_event_at: self.last_event_at().map(system_time_to_rfc3339),
|
||||||
cwd: self.cwd().map(str::to_string),
|
cwd: self.cwd().map(str::to_string),
|
||||||
stalled: self.is_stalled_at(now, stall_after),
|
stalled: self.is_stalled_at(now, stall_after),
|
||||||
|
|
@ -627,12 +627,18 @@ fn system_time_to_rfc3339(time: SystemTime) -> String {
|
||||||
/// A supervised pane — one agent occupying one PTY/session slot.
|
/// A supervised pane — one agent occupying one PTY/session slot.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct Pane {
|
pub struct Pane {
|
||||||
/// Colibri-owned pane id. Distinct from the Pi session id.
|
/// Colibri-owned pane id. Distinct from the agent's own session id.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub agent: String,
|
pub agent: String,
|
||||||
pub state: AgentState,
|
pub state: AgentState,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
// `alias` keeps deserializing the legacy `pi_session_id` key (pre-zot wire
|
||||||
pub pi_session_id: Option<String>,
|
// format / persisted snapshots) onto the harness-neutral field.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "pi_session_id",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub session_id: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub last_event_at: Option<String>,
|
pub last_event_at: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -737,7 +743,7 @@ mod tests {
|
||||||
id: "p1".into(),
|
id: "p1".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Working,
|
state: AgentState::Working,
|
||||||
pi_session_id: Some("pi-session-1".into()),
|
session_id: Some("pi-session-1".into()),
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: Some("/repo".into()),
|
cwd: Some("/repo".into()),
|
||||||
stalled: false,
|
stalled: false,
|
||||||
|
|
@ -746,7 +752,7 @@ mod tests {
|
||||||
id: "p2".into(),
|
id: "p2".into(),
|
||||||
agent: "pi".into(),
|
agent: "pi".into(),
|
||||||
state: AgentState::Blocked,
|
state: AgentState::Blocked,
|
||||||
pi_session_id: None,
|
session_id: None,
|
||||||
last_event_at: None,
|
last_event_at: None,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
stalled: true,
|
stalled: true,
|
||||||
|
|
@ -779,7 +785,7 @@ mod tests {
|
||||||
assert_eq!(pane.state, AgentState::Done);
|
assert_eq!(pane.state, AgentState::Done);
|
||||||
assert_eq!(pane.id, "pane-a");
|
assert_eq!(pane.id, "pane-a");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pane.pi_session_id.as_deref(),
|
pane.session_id.as_deref(),
|
||||||
Some("019e5e59-6645-7e21-aca2-b57ccf0f8578")
|
Some("019e5e59-6645-7e21-aca2-b57ccf0f8578")
|
||||||
);
|
);
|
||||||
assert_eq!(pane.cwd.as_deref(), Some("/home/clawdija/clawdie-ai"));
|
assert_eq!(pane.cwd.as_deref(), Some("/home/clawdija/clawdie-ai"));
|
||||||
|
|
@ -815,7 +821,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(update.pi_type, "session");
|
assert_eq!(update.pi_type, "session");
|
||||||
assert_eq!(update.observed_at, t(10));
|
assert_eq!(update.observed_at, t(10));
|
||||||
assert_eq!(ingestor.pi_session_id(), Some("pi-s"));
|
assert_eq!(ingestor.session_id(), Some("pi-s"));
|
||||||
assert_eq!(ingestor.cwd(), Some("/repo"));
|
assert_eq!(ingestor.cwd(), Some("/repo"));
|
||||||
assert_eq!(ingestor.last_event_at(), Some(t(10)));
|
assert_eq!(ingestor.last_event_at(), Some(t(10)));
|
||||||
|
|
||||||
|
|
@ -825,7 +831,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn supervisor_keeps_colibri_pane_id_separate_from_pi_session_id() {
|
fn supervisor_keeps_colibri_pane_id_separate_from_session_id() {
|
||||||
let mut supervisor = PaneSupervisor::new();
|
let mut supervisor = PaneSupervisor::new();
|
||||||
supervisor.attach_pane_at("pane-1", "pi", t(0));
|
supervisor.attach_pane_at("pane-1", "pi", t(0));
|
||||||
supervisor.ingest_line_at(
|
supervisor.ingest_line_at(
|
||||||
|
|
@ -840,8 +846,8 @@ mod tests {
|
||||||
.pop()
|
.pop()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(pane.id, "pane-1");
|
assert_eq!(pane.id, "pane-1");
|
||||||
assert_eq!(pane.pi_session_id.as_deref(), Some("pi-session-1"));
|
assert_eq!(pane.session_id.as_deref(), Some("pi-session-1"));
|
||||||
assert_ne!(pane.id, pane.pi_session_id.unwrap());
|
assert_ne!(pane.id, pane.session_id.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -861,7 +867,7 @@ mod tests {
|
||||||
|
|
||||||
let pane = supervisor.get("pane-a").unwrap();
|
let pane = supervisor.get("pane-a").unwrap();
|
||||||
assert_eq!(pane.state(), AgentState::Working);
|
assert_eq!(pane.state(), AgentState::Working);
|
||||||
assert_eq!(pane.pi_session_id(), Some("pi-s"));
|
assert_eq!(pane.session_id(), Some("pi-s"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pane.last_event_at(),
|
pane.last_event_at(),
|
||||||
Some(t(100) + Duration::from_millis(3))
|
Some(t(100) + Duration::from_millis(3))
|
||||||
|
|
@ -896,7 +902,7 @@ mod tests {
|
||||||
|
|
||||||
let pane = supervisor.get("pane-reader").unwrap();
|
let pane = supervisor.get("pane-reader").unwrap();
|
||||||
assert_eq!(pane.state(), AgentState::Done);
|
assert_eq!(pane.state(), AgentState::Done);
|
||||||
assert_eq!(pane.pi_session_id(), Some("pi-s"));
|
assert_eq!(pane.session_id(), Some("pi-s"));
|
||||||
assert_eq!(pane.last_event_at(), Some(t(13)));
|
assert_eq!(pane.last_event_at(), Some(t(13)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ struct RuntimeInventory {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pi: Option<String>,
|
pi: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
zot: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
npm_prefix: Option<String>,
|
npm_prefix: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
package_manager: Option<String>,
|
package_manager: Option<String>,
|
||||||
|
|
@ -156,6 +158,23 @@ fn detect_pi_version() -> Option<String> {
|
||||||
.find_map(|candidate| pi_package_version_from_bin(&candidate))
|
.find_map(|candidate| pi_package_version_from_bin(&candidate))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect the installed zot agent version. zot is a single Go binary (not an
|
||||||
|
/// npm package), so this is a plain `--version` probe across `$ZOT_BIN`, PATH,
|
||||||
|
/// and the usual candidate locations.
|
||||||
|
fn detect_zot_version() -> Option<String> {
|
||||||
|
if let Ok(zot_bin) = env::var("ZOT_BIN") {
|
||||||
|
if let Some(version) = command_output(&zot_bin, &["--version"]) {
|
||||||
|
return Some(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(version) = command_output("zot", &["--version"]) {
|
||||||
|
return Some(version);
|
||||||
|
}
|
||||||
|
command_candidates("zot")
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|candidate| command_output(&candidate, &["--version"]))
|
||||||
|
}
|
||||||
|
|
||||||
fn detect_package_manager() -> Option<String> {
|
fn detect_package_manager() -> Option<String> {
|
||||||
if command_output("pkg", &["--version"]).is_some() {
|
if command_output("pkg", &["--version"]).is_some() {
|
||||||
return Some("pkg".to_string());
|
return Some("pkg".to_string());
|
||||||
|
|
@ -180,6 +199,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
node: command_output("node", &["--version"]),
|
node: command_output("node", &["--version"]),
|
||||||
npm: command_output("npm", &["--version"]),
|
npm: command_output("npm", &["--version"]),
|
||||||
pi: detect_pi_version(),
|
pi: detect_pi_version(),
|
||||||
|
zot: detect_zot_version(),
|
||||||
npm_prefix: command_output("npm", &["config", "get", "prefix"]),
|
npm_prefix: command_output("npm", &["config", "get", "prefix"]),
|
||||||
package_manager: detect_package_manager(),
|
package_manager: detect_package_manager(),
|
||||||
iso_npm_globals_pin: BTreeMap::new(),
|
iso_npm_globals_pin: BTreeMap::new(),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue