diff --git a/AGENTS.md b/AGENTS.md index 22a4006..a392e61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,6 +102,18 @@ cargo test --workspace 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 | Gate | Target | Status | diff --git a/crates/colibri-client/src/lib.rs b/crates/colibri-client/src/lib.rs index 4dfb537..9ae05b8 100644 --- a/crates/colibri-client/src/lib.rs +++ b/crates/colibri-client/src/lib.rs @@ -327,7 +327,7 @@ mod tests { "id": "pane-1", "agent": "pi", "state": "working", - "pi_session_id": "pi-session-1", + "session_id": "pi-session-1", "last_event_at": "2026-05-27T11:59:59.000Z", "cwd": "/repo" }] diff --git a/crates/colibri-client/tests/live_socket_check.rs b/crates/colibri-client/tests/live_socket_check.rs index dc99dcc..0099d72 100644 --- a/crates/colibri-client/tests/live_socket_check.rs +++ b/crates/colibri-client/tests/live_socket_check.rs @@ -130,7 +130,7 @@ async fn daemon_client_live_socket_check_with_local_fake_agent() { .iter() .find(|pane| pane.id == agent_id) .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()); 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); // 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 let kill = client.kill_agent(&pane_a.id).await.unwrap(); diff --git a/crates/colibri-contracts/src/lib.rs b/crates/colibri-contracts/src/lib.rs index 3b49a1d..202d45c 100644 --- a/crates/colibri-contracts/src/lib.rs +++ b/crates/colibri-contracts/src/lib.rs @@ -49,6 +49,8 @@ pub struct RuntimeInventory { #[serde(default, skip_serializing_if = "Option::is_none")] pub pi: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub zot: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub npm_prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub package_manager: Option, diff --git a/crates/colibri-daemon/src/main.rs b/crates/colibri-daemon/src/main.rs index d1fb603..194d0f1 100644 --- a/crates/colibri-daemon/src/main.rs +++ b/crates/colibri-daemon/src/main.rs @@ -74,7 +74,7 @@ async fn main() -> Result<(), Box> { let socket_handle = 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; // no-op unless COLIBRI_AUTOSPAWN is set and a DeepSeek key is present. socket::autospawn_agent_if_configured(&state).await; diff --git a/crates/colibri-daemon/src/socket.rs b/crates/colibri-daemon/src/socket.rs index 29042e2..e38cb24 100644 --- a/crates/colibri-daemon/src/socket.rs +++ b/crates/colibri-daemon/src/socket.rs @@ -416,9 +416,7 @@ pub async fn autospawn_agent_if_configured(state: &SharedState) { return; } if state.config.deepseek_api_key.is_none() { - info!( - "autospawn: DEEPSEEK_API_KEY not set; skipping (operator can add it via Join Hive)" - ); + info!("autospawn: DEEPSEEK_API_KEY not set; skipping (operator can add it via Join Hive)"); return; } @@ -438,22 +436,13 @@ pub async fn autospawn_agent_if_configured(state: &SharedState) { return; } - // Default argv depends on the harness: zot is a request/response peer - // driven over stdin (`zot rpc`), while pi self-drives (`pi --mode json`). - // `--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" - }; + // Default argv depends on the harness (see default_agent_args). Override + // wholesale with COLIBRI_AUTOSPAWN_ARGS. let args: Vec = std::env::var("COLIBRI_AUTOSPAWN_ARGS") .ok() .filter(|s| !s.trim().is_empty()) - .unwrap_or_else(|| default_args.to_string()) - .split_whitespace() - .map(str::to_string) - .collect(); + .map(|s| s.split_whitespace().map(str::to_string).collect()) + .unwrap_or_else(|| default_agent_args(&agent_binary)); info!(binary = %agent_binary, ?args, "autospawn: spawning agent on host (DeepSeek-backed)"); @@ -554,6 +543,20 @@ fn basename(path: &str) -> 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 { + if basename(binary) == "zot" { + vec!["rpc".to_string()] + } else { + vec!["--mode".to_string(), "json".to_string()] + } +} + fn env_truthy(name: &str) -> bool { std::env::var(name) .map(|v| { @@ -565,6 +568,10 @@ fn env_truthy(name: &str) -> bool { .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( state: &SharedState, provider_str: String, @@ -590,10 +597,12 @@ async fn cmd_spawn_agent( let args = local_args.unwrap_or_default(); (model.clone(), args) } else { - ( - std::env::var("COLIBRI_AGENT_BINARY").unwrap_or_else(|_| "hermes-agent".to_string()), - vec!["--mode".to_string(), "json".to_string()], - ) + // Non-local provider: a managed agent harness. Default to zot (the + // current default harness); argv follows the harness via + // 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 @@ -1093,7 +1102,7 @@ mod tests { let data = response.data.unwrap(); assert_eq!(data["schema"], "clawdie.glasspane.snapshot.v1"); 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] @@ -1130,7 +1139,7 @@ mod tests { .find(|pane| pane.id == pane_id) .unwrap(); 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] diff --git a/crates/colibri-daemon/src/spawner.rs b/crates/colibri-daemon/src/spawner.rs index f9f1ab6..ca182af 100644 --- a/crates/colibri-daemon/src/spawner.rs +++ b/crates/colibri-daemon/src/spawner.rs @@ -317,11 +317,7 @@ impl RpcSender { /// 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. pub async fn send_prompt(&self, message: &str) -> std::io::Result { - let id = (self - .seq - .fetch_add(1, std::sync::atomic::Ordering::SeqCst) - + 1) - .to_string(); + let id = (self.seq.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1).to_string(); let line = build_rpc_prompt(&id, message); let mut guard = self.stdin.lock().await; let stdin = guard.as_mut().ok_or_else(|| { @@ -552,7 +548,7 @@ pub fn jail_wrap( /// Configuration for spawning an agent subprocess. #[derive(Debug, Clone, Serialize, Deserialize)] 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, /// Command-line arguments. #[serde(default)] diff --git a/crates/colibri-daemon/tests/glasspane_integration.rs b/crates/colibri-daemon/tests/glasspane_integration.rs index bf5f7eb..dc0abb0 100644 --- a/crates/colibri-daemon/tests/glasspane_integration.rs +++ b/crates/colibri-daemon/tests/glasspane_integration.rs @@ -29,7 +29,7 @@ mod integration { ingestor.ingest_line_at(line, t + Duration::from_secs(i as u64)); } 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) @@ -81,7 +81,7 @@ mod integration { id: "a".into(), agent: "pi".into(), state: AgentState::Working, - pi_session_id: Some("s1".into()), + session_id: Some("s1".into()), last_event_at: Some("2026-05-27T10:00:00Z".into()), cwd: None, stalled: false, @@ -90,7 +90,7 @@ mod integration { id: "b".into(), agent: "pi".into(), state: AgentState::Blocked, - pi_session_id: Some("s2".into()), + session_id: Some("s2".into()), last_event_at: Some("2026-05-27T10:00:01Z".into()), cwd: None, stalled: false, @@ -99,7 +99,7 @@ mod integration { id: "c".into(), agent: "pi".into(), state: AgentState::Idle, - pi_session_id: None, + session_id: None, last_event_at: None, cwd: None, stalled: false, @@ -124,7 +124,7 @@ mod integration { id: "p1".into(), agent: "pi".into(), 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()), cwd: None, stalled: false, @@ -141,7 +141,7 @@ mod integration { fn default_ingestor_is_idle() { let ingestor = PiJsonlIngestor::default(); assert_eq!(ingestor.state(), AgentState::Idle); - assert!(ingestor.pi_session_id().is_none()); + assert!(ingestor.session_id().is_none()); } /// Queue update blocks agent diff --git a/crates/colibri-daemon/tests/pi_spawn_live.rs b/crates/colibri-daemon/tests/pi_spawn_live.rs index acdc0ea..21fde46 100644 --- a/crates/colibri-daemon/tests/pi_spawn_live.rs +++ b/crates/colibri-daemon/tests/pi_spawn_live.rs @@ -4,7 +4,7 @@ //! Uses scripts/fake-pi-agent.py which emits the colibri-pi-events JSONL taxonomy. //! //! 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::process::Stdio; @@ -79,13 +79,13 @@ async fn pi_spawn_path_produces_correct_glasspane_state() { "expected Done after full agent run" ); assert!( - pane.pi_session_id.is_some(), - "expected pi_session_id from session event, got {:?}", - pane.pi_session_id + pane.session_id.is_some(), + "expected session_id from session event, got {:?}", + pane.session_id ); eprintln!( "✓ Pi spawn proof: {} accepted, state={:?}, session={:?}", - accepted, pane.state, pane.pi_session_id + accepted, pane.state, pane.session_id ); } diff --git a/crates/colibri-glasspane-tui/src/main.rs b/crates/colibri-glasspane-tui/src/main.rs index ca9d526..f20edb0 100644 --- a/crates/colibri-glasspane-tui/src/main.rs +++ b/crates/colibri-glasspane-tui/src/main.rs @@ -74,7 +74,7 @@ struct App { // harness additions status_msg: Option<(String, u8)>, // (message, ttl ticks) detail_pane: Option, // index into snapshot.panes, or None - session_filter: Option, // filter by pi_session_id, or None + session_filter: Option, // filter by session_id, or None sessions: Vec, // all known session IDs session_idx: usize, // which session is selected } @@ -113,7 +113,7 @@ impl App { Some(sid) => snap .panes .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(), None => snap.panes.iter().collect(), } @@ -132,7 +132,7 @@ impl App { let mut ids: Vec = snap .panes .iter() - .filter_map(|p| p.pi_session_id.clone()) + .filter_map(|p| p.session_id.clone()) .collect(); ids.sort(); ids.dedup(); @@ -377,7 +377,7 @@ impl App { format!("{:?}", p.state), 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(if p.stalled { "⚠" } else { "" }), ]) @@ -429,7 +429,7 @@ impl App { ), Span::raw(" "), 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![ Span::styled("CWD: ", Style::default().add_modifier(Modifier::BOLD)), @@ -649,7 +649,7 @@ mod tests { id: "a".into(), agent: "pi".into(), state: AgentState::Working, - pi_session_id: Some("s1".into()), + session_id: Some("s1".into()), last_event_at: None, cwd: None, stalled: false, @@ -658,7 +658,7 @@ mod tests { id: "b".into(), agent: "pi".into(), state: AgentState::Idle, - pi_session_id: Some("s2".into()), + session_id: Some("s2".into()), last_event_at: None, cwd: None, stalled: false, @@ -693,7 +693,7 @@ mod tests { id: "a".into(), agent: "pi".into(), state: AgentState::Working, - pi_session_id: Some("s2".into()), + session_id: Some("s2".into()), last_event_at: None, cwd: None, stalled: false, @@ -702,7 +702,7 @@ mod tests { id: "b".into(), agent: "pi".into(), state: AgentState::Working, - pi_session_id: Some("s1".into()), + session_id: Some("s1".into()), last_event_at: None, cwd: None, stalled: false, @@ -711,7 +711,7 @@ mod tests { id: "c".into(), agent: "pi".into(), state: AgentState::Working, - pi_session_id: Some("s1".into()), + session_id: Some("s1".into()), last_event_at: None, cwd: None, stalled: false, diff --git a/crates/colibri-glasspane/src/lib.rs b/crates/colibri-glasspane/src/lib.rs index 3bf16b7..2b4a687 100644 --- a/crates/colibri-glasspane/src/lib.rs +++ b/crates/colibri-glasspane/src/lib.rs @@ -201,7 +201,7 @@ pub fn pane_from_jsonl_with_id( id: pane_id.into(), agent: agent.into(), state: ingestor.state, - pi_session_id: ingestor.pi_session_id, + session_id: ingestor.session_id, last_event_at: None, cwd: ingestor.cwd, stalled: false, @@ -223,7 +223,7 @@ pub fn pane_from_jsonl(agent: impl Into, jsonl: &str) -> Pane { pub struct PiStreamUpdate { pub pi_type: String, pub state: AgentState, - pub pi_session_id: Option, + pub session_id: Option, pub cwd: Option, pub observed_at: SystemTime, } @@ -244,7 +244,7 @@ pub struct PaneReaderStats { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PiJsonlIngestor { state: AgentState, - pi_session_id: Option, + session_id: Option, cwd: Option, last_event_at: Option, /// Which harness produced the stream. Pi events are read directly; zot @@ -256,7 +256,7 @@ impl Default for PiJsonlIngestor { fn default() -> Self { Self { state: AgentState::Idle, - pi_session_id: None, + session_id: None, cwd: None, last_event_at: None, runtime: AgentRuntime::Pi, @@ -277,8 +277,8 @@ impl PiJsonlIngestor { self.state } - pub fn pi_session_id(&self) -> Option<&str> { - self.pi_session_id.as_deref() + pub fn session_id(&self) -> Option<&str> { + self.session_id.as_deref() } pub fn cwd(&self) -> Option<&str> { @@ -312,7 +312,7 @@ impl PiJsonlIngestor { if ty == "session" || ty == "session_started" { 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) { self.cwd = Some(cwd.to_string()); @@ -322,7 +322,7 @@ impl PiJsonlIngestor { Some(PiStreamUpdate { pi_type: ty.to_string(), state: self.state, - pi_session_id: self.pi_session_id.clone(), + session_id: self.session_id.clone(), cwd: self.cwd.clone(), observed_at, }) @@ -373,8 +373,8 @@ impl SupervisedPane { self.ingestor.state() } - pub fn pi_session_id(&self) -> Option<&str> { - self.ingestor.pi_session_id() + pub fn session_id(&self) -> Option<&str> { + self.ingestor.session_id() } pub fn cwd(&self) -> Option<&str> { @@ -407,7 +407,7 @@ impl SupervisedPane { id: self.id.clone(), agent: self.agent.clone(), 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), cwd: self.cwd().map(str::to_string), 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. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 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 agent: String, pub state: AgentState, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pi_session_id: Option, + // `alias` keeps deserializing the legacy `pi_session_id` key (pre-zot wire + // format / persisted snapshots) onto the harness-neutral field. + #[serde( + default, + alias = "pi_session_id", + skip_serializing_if = "Option::is_none" + )] + pub session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub last_event_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -737,7 +743,7 @@ mod tests { id: "p1".into(), agent: "pi".into(), state: AgentState::Working, - pi_session_id: Some("pi-session-1".into()), + session_id: Some("pi-session-1".into()), last_event_at: None, cwd: Some("/repo".into()), stalled: false, @@ -746,7 +752,7 @@ mod tests { id: "p2".into(), agent: "pi".into(), state: AgentState::Blocked, - pi_session_id: None, + session_id: None, last_event_at: None, cwd: None, stalled: true, @@ -779,7 +785,7 @@ mod tests { assert_eq!(pane.state, AgentState::Done); assert_eq!(pane.id, "pane-a"); assert_eq!( - pane.pi_session_id.as_deref(), + pane.session_id.as_deref(), Some("019e5e59-6645-7e21-aca2-b57ccf0f8578") ); assert_eq!(pane.cwd.as_deref(), Some("/home/clawdija/clawdie-ai")); @@ -815,7 +821,7 @@ mod tests { .unwrap(); assert_eq!(update.pi_type, "session"); 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.last_event_at(), Some(t(10))); @@ -825,7 +831,7 @@ mod tests { } #[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(); supervisor.attach_pane_at("pane-1", "pi", t(0)); supervisor.ingest_line_at( @@ -840,8 +846,8 @@ mod tests { .pop() .unwrap(); assert_eq!(pane.id, "pane-1"); - assert_eq!(pane.pi_session_id.as_deref(), Some("pi-session-1")); - assert_ne!(pane.id, pane.pi_session_id.unwrap()); + assert_eq!(pane.session_id.as_deref(), Some("pi-session-1")); + assert_ne!(pane.id, pane.session_id.unwrap()); } #[test] @@ -861,7 +867,7 @@ mod tests { let pane = supervisor.get("pane-a").unwrap(); 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!( pane.last_event_at(), Some(t(100) + Duration::from_millis(3)) @@ -896,7 +902,7 @@ mod tests { let pane = supervisor.get("pane-reader").unwrap(); 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))); } diff --git a/src/bin/runtime_inventory.rs b/src/bin/runtime_inventory.rs index 8a07096..4f184ec 100644 --- a/src/bin/runtime_inventory.rs +++ b/src/bin/runtime_inventory.rs @@ -21,6 +21,8 @@ struct RuntimeInventory { #[serde(skip_serializing_if = "Option::is_none")] pi: Option, #[serde(skip_serializing_if = "Option::is_none")] + zot: Option, + #[serde(skip_serializing_if = "Option::is_none")] npm_prefix: Option, #[serde(skip_serializing_if = "Option::is_none")] package_manager: Option, @@ -156,6 +158,23 @@ fn detect_pi_version() -> Option { .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 { + 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 { if command_output("pkg", &["--version"]).is_some() { return Some("pkg".to_string()); @@ -180,6 +199,7 @@ fn main() -> Result<(), Box> { node: command_output("node", &["--version"]), npm: command_output("npm", &["--version"]), pi: detect_pi_version(), + zot: detect_zot_version(), npm_prefix: command_output("npm", &["config", "get", "prefix"]), package_manager: detect_package_manager(), iso_npm_globals_pin: BTreeMap::new(),