diff --git a/crates/common/src/agent_events.rs b/crates/common/src/agent_events.rs index c015fbe5e..fc8bce93f 100644 --- a/crates/common/src/agent_events.rs +++ b/crates/common/src/agent_events.rs @@ -1,15 +1,20 @@ use std::fmt; +use std::io; use std::sync::OnceLock; use chrono::{DateTime, Utc}; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::json; use crate::identity::{ - validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementPosture, + validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementExecution, + PlacementPosture, }; pub const AGENT_EVENT_CHANNEL_MAX_BYTES: usize = 64; +const PURE_AGENT_ROUTER: &str = "agent_hub"; +const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; /// Canonical set of agent event categories. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -73,6 +78,8 @@ pub struct AgentEvent { pub agent_id: String, pub orchestration_session_id: String, pub run_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_run_id: Option, // Attribution + correlation (optional) #[serde(default, skip_serializing_if = "Option::is_none")] @@ -84,6 +91,8 @@ pub struct AgentEvent { #[serde(default, skip_serializing_if = "Option::is_none")] pub world_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub world_generation: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub cmd_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub span_id: Option, @@ -116,6 +125,8 @@ struct AgentEventDef { orchestration_session_id: String, run_id: String, #[serde(default)] + parent_run_id: Option, + #[serde(default)] backend_id: Option, #[serde(default)] thread_id: Option, @@ -124,12 +135,24 @@ struct AgentEventDef { #[serde(default)] world_id: Option, #[serde(default)] + world_generation: Option, + #[serde(default)] cmd_id: Option, #[serde(default)] span_id: Option, #[serde(default, deserialize_with = "deserialize_sanitized_channel")] channel: Option, #[serde(default)] + client: Option, + #[serde(default)] + router: Option, + #[serde(default)] + protocol: Option, + #[serde(default)] + provider: Option, + #[serde(default)] + auth_authority: Option, + #[serde(default)] identity_tuple: Option, #[serde(default)] placement_posture: Option, @@ -173,11 +196,13 @@ impl AgentEvent { kind, orchestration_session_id: orchestration_session_id.into(), run_id: run_id.into(), + parent_run_id: None, data, backend_id: None, thread_id: None, role: None, world_id: None, + world_generation: None, cmd_id: None, span_id: None, channel: None, @@ -253,12 +278,83 @@ impl AgentEvent { self.placement_posture.as_ref(), ) } + + pub fn set_pure_agent_telemetry_identity(&mut self, client: impl Into) { + if self.identity_tuple.is_none() { + self.identity_tuple = Some(IdentityTuple { + client: client.into(), + router: PURE_AGENT_ROUTER.to_string(), + protocol: PURE_AGENT_PROTOCOL.to_string(), + provider: None, + auth_authority: None, + }); + } + + if self.placement_posture.is_none() { + self.placement_posture = Some(PlacementPosture { + execution: if self.world_id.is_some() { + PlacementExecution::InWorld + } else { + PlacementExecution::HostOnly + }, + host_to_world_bridge: None, + }); + } + } + + pub fn to_trace_record(&self) -> Result { + let mut entry = serde_json::to_value(self)?; + let Some(obj) = entry.as_object_mut() else { + return Err(serde_json::Error::io(io::Error::other( + "agent event must serialize as a JSON object", + ))); + }; + + if let Some(tuple) = self.identity_tuple.as_ref() { + obj.insert("client".to_string(), json!(tuple.client)); + obj.insert("router".to_string(), json!(tuple.router)); + obj.insert("protocol".to_string(), json!(tuple.protocol)); + + match tuple.provider.as_deref() { + Some(provider) => { + obj.insert("provider".to_string(), json!(provider)); + } + None => { + obj.remove("provider"); + } + } + + match tuple.auth_authority.as_deref() { + Some(auth_authority) => { + obj.insert("auth_authority".to_string(), json!(auth_authority)); + } + None => { + obj.remove("auth_authority"); + } + } + } + + Ok(entry) + } } impl TryFrom for AgentEvent { type Error = String; fn try_from(value: AgentEventDef) -> Result { + let identity_tuple = value.identity_tuple.or_else(|| { + let client = value.client?; + let router = value.router?; + let protocol = value.protocol?; + Some(IdentityTuple { + client, + router, + protocol, + provider: value.provider, + auth_authority: value.auth_authority, + }) + }); + let event = Self { ts: value.ts, kind: value.kind, @@ -266,14 +362,16 @@ impl TryFrom for AgentEvent { agent_id: value.agent_id, orchestration_session_id: value.orchestration_session_id, run_id: value.run_id, + parent_run_id: value.parent_run_id, backend_id: value.backend_id, thread_id: value.thread_id, role: value.role, world_id: value.world_id, + world_generation: value.world_generation, cmd_id: value.cmd_id, span_id: value.span_id, channel: value.channel, - identity_tuple: value.identity_tuple, + identity_tuple, placement_posture: value.placement_posture, project: value.project, }; diff --git a/crates/common/src/agent_identity.rs b/crates/common/src/agent_identity.rs new file mode 100644 index 000000000..36909175a --- /dev/null +++ b/crates/common/src/agent_identity.rs @@ -0,0 +1,3 @@ +pub fn derive_agent_backend_id(kind: &str, agent_id: &str) -> String { + format!("{}:{}", kind.trim(), agent_id.trim()) +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 5a3086ab0..02515de34 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; pub mod agent_events; +pub mod agent_identity; pub mod fs_diff; pub mod identity; pub mod manager_manifest; @@ -12,6 +13,7 @@ pub mod settings; pub mod world_exec_guard; pub use agent_events::{AgentEvent, AgentEventKind}; +pub use agent_identity::derive_agent_backend_id; pub use fs_diff::FsDiff; pub use identity::{ validate_identity_tuple_and_placement_posture, IdentityTuple, PlacementExecution, diff --git a/crates/common/tests/agent_hub_event_envelope_schema.rs b/crates/common/tests/agent_hub_event_envelope_schema.rs index ec3b2efd1..3c9a44c4b 100644 --- a/crates/common/tests/agent_hub_event_envelope_schema.rs +++ b/crates/common/tests/agent_hub_event_envelope_schema.rs @@ -50,6 +50,40 @@ fn envelope_missing_required_fields_is_rejected() { ); } +#[test] +fn parent_run_id_roundtrips_when_present() { + let mut value = minimal_valid_envelope_json(); + value.as_object_mut().expect("envelope object").insert( + "parent_run_id".to_string(), + json!("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f11"), + ); + + let event: AgentEvent = serde_json::from_value(value).expect("deserialize AgentEvent"); + let roundtrip = serde_json::to_value(&event).expect("serialize AgentEvent"); + + assert_eq!( + roundtrip.get("parent_run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f11"), + "expected parent_run_id to survive roundtrip; got: {roundtrip}" + ); +} + +#[test] +fn parent_run_id_omits_by_field_absence_when_unset() { + let event: AgentEvent = + serde_json::from_value(minimal_valid_envelope_json()).expect("deserialize AgentEvent"); + let roundtrip = serde_json::to_value(&event).expect("serialize AgentEvent"); + + assert!( + roundtrip.get("parent_run_id").is_none(), + "expected parent_run_id to omit when unset; got: {roundtrip}" + ); + assert!( + !roundtrip.to_string().contains("\"parent_run_id\":null"), + "parent_run_id must not serialize as null when unset: {roundtrip}" + ); +} + #[test] fn safe_channel_roundtrips() { let mut value = minimal_valid_envelope_json(); @@ -360,6 +394,7 @@ fn trace_tuple_metadata_preserves_existing_join_keys() { "agent_id": "demo-agent", "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f10", "backend_id": "cli:codex", "world_id": "wld_test", "cmd_id": "cmd_test", @@ -396,6 +431,10 @@ fn trace_tuple_metadata_preserves_existing_join_keys() { roundtrip.get("span_id").and_then(Value::as_str), Some("spn_test") ); + assert_eq!( + roundtrip.get("parent_run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f10") + ); assert_eq!( roundtrip .pointer("/identity_tuple/router") diff --git a/crates/shell/src/builtins/world_gateway.rs b/crates/shell/src/builtins/world_gateway.rs index 0b2198818..11cb712dd 100644 --- a/crates/shell/src/builtins/world_gateway.rs +++ b/crates/shell/src/builtins/world_gateway.rs @@ -202,15 +202,7 @@ fn build_macos_gateway_client() -> anyhow::Result { }); } - let default_sock = macos_default_world_socket_path(); - if default_sock.exists() && probe_gateway_caps_uds(&default_sock) { - return Ok(MacosGatewayClient { - client: AgentClient::unix_socket(default_sock)?, - _forwarding: None, - }); - } - - if substrate_home_is_explicitly_set() { + if let Some(default_sock) = resolve_macos_host_gateway_socket() { return Ok(MacosGatewayClient { client: AgentClient::unix_socket(default_sock)?, _forwarding: None, @@ -247,17 +239,22 @@ fn resolve_macos_gateway_client_endpoint() -> MacosGatewayClientEndpoint { return MacosGatewayClientEndpoint::Unix(std::path::PathBuf::from(socket_path)); } - let default_sock = macos_default_world_socket_path(); - - if substrate_home_is_explicitly_set() - || (default_sock.exists() && probe_gateway_caps_uds(&default_sock)) - { - MacosGatewayClientEndpoint::Unix(default_sock) - } else { - MacosGatewayClientEndpoint::Tcp { + match resolve_macos_host_gateway_socket() { + Some(default_sock) => MacosGatewayClientEndpoint::Unix(default_sock), + None => MacosGatewayClientEndpoint::Tcp { host: "127.0.0.1".to_string(), port: 17788, - } + }, + } +} + +#[cfg(target_os = "macos")] +fn resolve_macos_host_gateway_socket() -> Option { + let default_sock = macos_default_world_socket_path(); + if default_sock.exists() && probe_gateway_caps_uds(&default_sock) { + Some(default_sock) + } else { + None } } @@ -282,11 +279,6 @@ fn probe_gateway_caps_uds(path: &std::path::Path) -> bool { } } -#[cfg_attr(not(target_os = "macos"), allow(dead_code))] -fn substrate_home_is_explicitly_set() -> bool { - std::env::var_os("SUBSTRATE_HOME").is_some_and(|value| !value.is_empty()) -} - #[cfg_attr(not(target_os = "macos"), allow(dead_code))] fn macos_default_world_socket_path() -> PathBuf { substrate_common::paths::substrate_home() @@ -1025,8 +1017,10 @@ where #[cfg(all(test, target_os = "macos"))] mod tests { use super::*; + use crate::execution::world_env_guard; fn with_env_var(key: &str, value: Option<&std::ffi::OsStr>, f: impl FnOnce() -> T) -> T { + let _guard = world_env_guard(); let prev = std::env::var_os(key); match value { Some(value) => std::env::set_var(key, value), @@ -1057,13 +1051,15 @@ mod tests { }); with_env_var("HOME", Some(home.as_os_str()), || { - with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { - match resolve_macos_gateway_client_endpoint() { - MacosGatewayClientEndpoint::Unix(path) => assert_eq!(path, sock), - MacosGatewayClientEndpoint::Tcp { .. } => { - panic!("expected unix endpoint when host socket exists") + with_env_var("SUBSTRATE_HOME", None, || { + with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { + match resolve_macos_gateway_client_endpoint() { + MacosGatewayClientEndpoint::Unix(path) => assert_eq!(path, sock), + MacosGatewayClientEndpoint::Tcp { .. } => { + panic!("expected unix endpoint when host socket exists") + } } - } + }) }) }); @@ -1076,6 +1072,29 @@ mod tests { let home = temp.path(); with_env_var("HOME", Some(home.as_os_str()), || { + with_env_var("SUBSTRATE_HOME", None, || { + with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { + match resolve_macos_gateway_client_endpoint() { + MacosGatewayClientEndpoint::Tcp { host, port } => { + assert_eq!(host, "127.0.0.1"); + assert_eq!(port, 17788); + } + MacosGatewayClientEndpoint::Unix(path) => { + panic!("expected tcp fallback when socket is missing, got {path:?}") + } + } + }) + }) + }); + } + + #[test] + fn macos_gateway_client_endpoint_falls_back_to_tcp_when_explicit_substrate_home_socket_missing() + { + let temp = tempfile::tempdir().expect("tempdir"); + let substrate_home = temp.path().join("isolated-substrate-home"); + + with_env_var("SUBSTRATE_HOME", Some(substrate_home.as_os_str()), || { with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { match resolve_macos_gateway_client_endpoint() { MacosGatewayClientEndpoint::Tcp { host, port } => { @@ -1083,7 +1102,9 @@ mod tests { assert_eq!(port, 17788); } MacosGatewayClientEndpoint::Unix(path) => { - panic!("expected tcp fallback when socket is missing, got {path:?}") + panic!( + "expected tcp fallback when explicit substrate home socket is missing, got {path:?}" + ) } } }) @@ -1099,16 +1120,18 @@ mod tests { std::fs::write(&sock, "").expect("create placeholder socket path"); with_env_var("HOME", Some(home.as_os_str()), || { - with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { - match resolve_macos_gateway_client_endpoint() { - MacosGatewayClientEndpoint::Tcp { host, port } => { - assert_eq!(host, "127.0.0.1"); - assert_eq!(port, 17788); - } - MacosGatewayClientEndpoint::Unix(path) => { - panic!("expected tcp fallback when socket is stale, got {path:?}") + with_env_var("SUBSTRATE_HOME", None, || { + with_env_var("SUBSTRATE_WORLD_SOCKET", None, || { + match resolve_macos_gateway_client_endpoint() { + MacosGatewayClientEndpoint::Tcp { host, port } => { + assert_eq!(host, "127.0.0.1"); + assert_eq!(port, 17788); + } + MacosGatewayClientEndpoint::Unix(path) => { + panic!("expected tcp fallback when socket is stale, got {path:?}") + } } - } + }) }) }); } @@ -1116,12 +1139,11 @@ mod tests { #[cfg(test)] mod classification_tests { - use super::{ - error_is_component_unavailable, macos_default_world_socket_path, - substrate_home_is_explicitly_set, - }; + use super::{error_is_component_unavailable, macos_default_world_socket_path}; + use crate::execution::world_env_guard; fn with_env_var(key: &str, value: Option<&std::ffi::OsStr>, f: impl FnOnce() -> T) -> T { + let _guard = world_env_guard(); let prev = std::env::var_os(key); match value { Some(value) => std::env::set_var(key, value), @@ -1156,7 +1178,6 @@ mod classification_tests { let substrate_home = temp.path().join("isolated-substrate-home"); with_env_var("SUBSTRATE_HOME", Some(substrate_home.as_os_str()), || { - assert!(substrate_home_is_explicitly_set()); assert_eq!( macos_default_world_socket_path(), substrate_home.join("sock/agent.sock") diff --git a/crates/shell/src/execution/agent_inventory.rs b/crates/shell/src/execution/agent_inventory.rs index dc66d4372..e44e9dbd7 100644 --- a/crates/shell/src/execution/agent_inventory.rs +++ b/crates/shell/src/execution/agent_inventory.rs @@ -6,7 +6,8 @@ use std::collections::BTreeMap; use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; -use substrate_broker::{validate_backend_id, Policy, WorldFsDenyEnforcement}; +use substrate_broker::{validate_backend_id, validate_dotted_id, Policy, WorldFsDenyEnforcement}; +use substrate_common::derive_agent_backend_id; use substrate_common::paths as substrate_paths; #[derive(Debug, Clone, Deserialize)] @@ -26,6 +27,8 @@ pub(crate) struct AgentConfigV1 { pub enabled: bool, pub kind: AgentConfigKind, #[serde(default)] + pub protocol: Option, + #[serde(default)] pub execution: AgentExecutionConfigV1, #[serde(default)] pub cli: Option, @@ -42,6 +45,15 @@ pub(crate) enum AgentConfigKind { Api, } +impl AgentConfigKind { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Cli => "cli", + Self::Api => "api", + } + } +} + #[derive(Debug, Clone, Default, Deserialize)] #[serde(default, deny_unknown_fields)] pub(crate) struct AgentExecutionConfigV1 { @@ -74,6 +86,12 @@ pub(crate) struct AgentApiAuthConfigV1 { #[derive(Debug, Clone, Default, Deserialize)] #[serde(default, deny_unknown_fields)] pub(crate) struct AgentCapabilitiesV1 { + pub session_start: bool, + pub session_resume: bool, + pub session_fork: bool, + pub session_stop: bool, + pub status_snapshot: bool, + pub event_stream: bool, pub llm: bool, pub mcp_client: bool, } @@ -84,6 +102,35 @@ pub(crate) struct AgentInventoryEntryV1 { pub file: AgentFileV1, } +impl AgentFileV1 { + pub(crate) fn derived_backend_id(&self) -> String { + derive_agent_backend_id(self.config.kind.as_str(), &self.id) + } + + pub(crate) fn effective_scope( + &self, + effective_config: &crate::execution::config_model::SubstrateConfig, + ) -> crate::execution::config_model::AgentExecutionScope { + self.config + .execution + .scope + .unwrap_or(effective_config.agents.defaults.execution.scope) + } +} + +impl AgentInventoryEntryV1 { + pub(crate) fn derived_backend_id(&self) -> String { + self.file.derived_backend_id() + } + + pub(crate) fn effective_scope( + &self, + effective_config: &crate::execution::config_model::SubstrateConfig, + ) -> crate::execution::config_model::AgentExecutionScope { + self.file.effective_scope(effective_config) + } +} + fn default_true() -> bool { true } @@ -159,14 +206,8 @@ pub(crate) fn resolve_gateway_backend_inventory_entry( )) })?; - let derived_backend_id = format!( - "{}:{}", - agent_kind_as_backend_kind(entry.file.config.kind), - entry.file.id - ); - if derived_backend_id != trimmed_backend_id - || backend_kind != agent_kind_as_backend_kind(entry.file.config.kind) - { + let derived_backend_id = entry.derived_backend_id(); + if derived_backend_id != trimmed_backend_id || backend_kind != entry.file.config.kind.as_str() { return Err(config_model::user_error(format!( "gateway backend '{}' does not match effective inventory item '{}' in {}", trimmed_backend_id, @@ -317,9 +358,39 @@ fn validate_agent_config(path: &Path, config: &AgentConfigV1) -> Result<()> { } } + if let Some(protocol) = &config.protocol { + let trimmed = protocol.trim(); + if trimmed.is_empty() { + return Err(config_model::user_error(format!( + "invalid agent file in {}: config.protocol must not be empty", + path.display() + ))); + } + if trimmed != protocol { + return Err(config_model::user_error(format!( + "invalid agent file in {}: config.protocol must not include leading or trailing whitespace", + path.display() + ))); + } + validate_dotted_id(protocol).map_err(|_| { + config_model::user_error(format!( + "invalid agent file in {}: config.protocol '{}' must be a lowercase dotted id", + path.display(), + protocol + )) + })?; + } + + let _ = &config.protocol; let _ = config.execution.scope; let _ = config.cli.as_ref().and_then(|cli| cli.mode); let _ = config.enabled; + let _ = config.capabilities.session_start; + let _ = config.capabilities.session_resume; + let _ = config.capabilities.session_fork; + let _ = config.capabilities.session_stop; + let _ = config.capabilities.status_snapshot; + let _ = config.capabilities.event_stream; let _ = config.capabilities.llm; let _ = config.capabilities.mcp_client; @@ -609,10 +680,3 @@ fn validate_overlay_subset( } Ok(()) } - -fn agent_kind_as_backend_kind(kind: AgentConfigKind) -> &'static str { - match kind { - AgentConfigKind::Cli => "cli", - AgentConfigKind::Api => "api", - } -} diff --git a/crates/shell/src/execution/agents_cmd.rs b/crates/shell/src/execution/agents_cmd.rs index 7512ef0f9..f21975641 100644 --- a/crates/shell/src/execution/agents_cmd.rs +++ b/crates/shell/src/execution/agents_cmd.rs @@ -1,9 +1,70 @@ -use crate::execution::agent_inventory::{discover_agent_files, validate_agent_file}; -use crate::execution::cli::{AgentsAction, AgentsCmd, Cli}; -use crate::execution::config_model; -use anyhow::Result; +use crate::execution::agent_inventory::{ + discover_agent_files, load_effective_agent_inventory, validate_agent_file, AgentCapabilitiesV1, + AgentInventoryEntryV1, +}; +use crate::execution::cli::{ + AgentAction, AgentCmd, AgentDoctorArgs, AgentScopeArg, AgentViewArgs, AgentsAction, AgentsCmd, + Cli, +}; +use crate::execution::config_model::{ + self, AgentExecutionScope, CliConfigOverrides, SubstrateConfig, +}; +use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use std::collections::{BTreeMap, BTreeSet}; use std::env; -use std::path::PathBuf; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use substrate_broker::Policy; +use substrate_common::paths as substrate_paths; +use substrate_common::{AgentEvent, PlacementExecution}; + +const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; +const PURE_AGENT_ROUTER: &str = "agent_hub"; +const NESTED_ROUTER: &str = "substrate_gateway"; +const MEMBER_ROLE: &str = "member"; +const ORCHESTRATOR_ROLE: &str = "orchestrator"; + +pub(crate) fn handle_agent_command(cmd: &AgentCmd, cli: &Cli) -> i32 { + match &cmd.action { + AgentAction::List(args) => match run_list(args, cli) { + Ok(()) => 0, + Err(err) if config_model::is_user_error(&err) => { + eprintln!("{err}"); + 2 + } + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }, + AgentAction::Status(args) => match run_status(args, cli) { + Ok(()) => 0, + Err(err) if config_model::is_user_error(&err) => { + eprintln!("{err}"); + 2 + } + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }, + AgentAction::Doctor(args) => match run_doctor(args, cli) { + Ok(code) => code, + Err(err) if config_model::is_user_error(&err) => { + eprintln!("{err}"); + 2 + } + Err(err) => { + eprintln!("{err:#}"); + 1 + } + }, + } +} pub(crate) fn handle_agents_command(cmd: &AgentsCmd, _cli: &Cli) -> i32 { let result = match &cmd.action { @@ -35,3 +96,1310 @@ fn run_validate() -> Result<()> { Ok(()) } + +fn run_list(args: &AgentViewArgs, cli: &Cli) -> Result<()> { + let context = resolve_command_context(cli)?; + let report = build_list_report(&context, args); + render_list_report(&report, args.json)?; + Ok(()) +} + +fn run_status(args: &AgentViewArgs, cli: &Cli) -> Result<()> { + let context = resolve_command_context(cli)?; + let report = build_status_report(&context, args)?; + render_status_report(&report, args.json)?; + Ok(()) +} + +fn run_doctor(args: &AgentDoctorArgs, cli: &Cli) -> Result { + let report = build_doctor_report(cli)?; + let exit_code = doctor_exit_code(&report); + render_doctor_report(&report, args.json)?; + Ok(exit_code) +} + +struct AgentCommandContext { + effective_config: SubstrateConfig, + base_policy: Policy, + inventory: BTreeMap, +} + +fn resolve_command_context(cli: &Cli) -> Result { + let cwd = current_dir(); + let effective_config = resolve_effective_config(&cwd, cli)?; + let (base_policy, _) = substrate_broker::resolve_effective_policy_with_explain(&cwd, false) + .map_err(|err| config_model::user_error(err.to_string()))?; + let inventory = load_effective_agent_inventory(&cwd, &base_policy)?; + + Ok(AgentCommandContext { + effective_config, + base_policy, + inventory, + }) +} + +fn current_dir() -> PathBuf { + env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) +} + +fn cli_world_enabled(cli: &Cli) -> Option { + if cli.world { + Some(true) + } else if cli.no_world { + Some(false) + } else { + None + } +} + +fn resolve_effective_config(cwd: &Path, cli: &Cli) -> Result { + config_model::resolve_effective_config( + cwd, + &CliConfigOverrides { + world_enabled: cli_world_enabled(cli), + ..Default::default() + }, + ) +} + +#[derive(Clone, Serialize)] +struct ExecutionScopeJson<'a> { + scope: &'a str, +} + +#[derive(Serialize)] +struct CapabilitiesSummaryJson { + llm: bool, + mcp_client: bool, +} + +#[derive(Serialize)] +struct EligibilityJson<'a> { + state: &'a str, + reason: Option, +} + +#[derive(Serialize)] +struct ListAgentJson<'a> { + agent_id: String, + backend_id: String, + kind: &'a str, + execution: ExecutionScopeJson<'a>, + role: Option<&'a str>, + capabilities_summary: CapabilitiesSummaryJson, + eligibility: EligibilityJson<'a>, + protocol: &'a str, +} + +#[derive(Serialize)] +struct ListReportJson<'a> { + disabled: bool, + scope_filter: &'a str, + role_filter: Option<&'a str>, + agents: Vec>, +} + +fn build_list_report<'a>( + context: &'a AgentCommandContext, + args: &'a AgentViewArgs, +) -> ListReportJson<'a> { + let role_filter = normalized_role_filter(args.role.as_deref()); + let agents = if context.effective_config.agents.enabled { + context + .inventory + .values() + .filter_map(|entry| { + let scope = entry.effective_scope(&context.effective_config); + let role = role_for_entry(&entry.file.id, &context.effective_config); + if !matches_scope(scope, args.scope) || !matches_role(role, role_filter) { + return None; + } + + let backend_id = entry.derived_backend_id(); + let eligibility_reason = + eligibility_reason(entry, &context.effective_config, &context.base_policy); + let eligibility = if let Some(reason) = eligibility_reason { + EligibilityJson { + state: "denied", + reason: Some(reason), + } + } else { + EligibilityJson { + state: "allowed", + reason: None, + } + }; + + Some(ListAgentJson { + agent_id: entry.file.id.clone(), + backend_id, + kind: entry.file.config.kind.as_str(), + execution: ExecutionScopeJson { + scope: scope.as_str(), + }, + role, + capabilities_summary: CapabilitiesSummaryJson { + llm: entry.file.config.capabilities.llm, + mcp_client: entry.file.config.capabilities.mcp_client, + }, + eligibility, + protocol: PURE_AGENT_PROTOCOL, + }) + }) + .collect() + } else { + Vec::new() + }; + + ListReportJson { + disabled: !context.effective_config.agents.enabled, + scope_filter: args.scope.as_str(), + role_filter, + agents, + } +} + +fn render_list_report(report: &ListReportJson<'_>, json_mode: bool) -> Result<()> { + if json_mode { + println!("{}", serde_json::to_string_pretty(report)?); + return Ok(()); + } + + println!("disabled: {}", report.disabled); + println!( + "scope_filter: {}{}", + report.scope_filter, + report + .role_filter + .map(|role| format!(", role_filter: {role}")) + .unwrap_or_default() + ); + println!( + "agent_id\tbackend_id\tkind\texecution.scope\trole\tcapabilities\teligibility\tprotocol" + ); + + for agent in &report.agents { + let capabilities = capabilities_label(&agent.capabilities_summary); + let eligibility = if let Some(reason) = &agent.eligibility.reason { + format!("{}: {}", agent.eligibility.state, reason) + } else { + agent.eligibility.state.to_string() + }; + println!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + agent.agent_id, + agent.backend_id, + agent.kind, + agent.execution.scope, + agent.role.unwrap_or(""), + capabilities, + eligibility, + agent.protocol + ); + } + + Ok(()) +} + +#[derive(Clone, Serialize)] +struct StatusSessionJson { + orchestration_session_id: String, + agent_id: String, + backend_id: String, + client: String, + router: String, + protocol: String, + execution: ExecutionScopeJson<'static>, + role: Option, + last_event_at: String, + #[serde(skip_serializing_if = "Option::is_none")] + world_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + world_generation: Option, +} + +#[derive(Clone, Serialize)] +struct NestedParentJson { + orchestration_session_id: String, + agent_id: String, +} + +#[derive(Clone, Serialize)] +struct NestedLlmRecordJson { + parent: NestedParentJson, + run_id: String, + backend_id: String, + client: String, + router: String, + provider: String, + auth_authority: String, + protocol: String, +} + +#[derive(Serialize)] +struct StatusReportJson<'a> { + disabled: bool, + scope_filter: &'a str, + role_filter: Option<&'a str>, + orchestrator_agent_id: String, + sessions: Vec, + nested_llm_records: Vec, +} + +#[derive(Clone)] +struct SessionProjection { + last_event_ts: DateTime, + session: StatusSessionJson, + source: SessionProjectionSource, +} + +#[derive(Clone)] +struct SessionProjectionSource { + orchestration_session_id: String, + agent_id: String, + run_id: String, + ts: DateTime, + is_world_scoped: bool, + has_top_level_world_id: bool, + has_top_level_world_generation: bool, +} + +#[derive(Clone)] +struct NestedProjection { + sort_key: (String, String, String), + source: NestedProjectionSource, + backend_id: String, + client: String, + protocol: String, + provider: Option, + auth_authority: Option, +} + +#[derive(Clone)] +struct NestedProjectionSource { + orchestration_session_id: String, + agent_id: String, + run_id: String, + parent_run_id: Option, +} + +fn build_status_report<'a>( + context: &'a AgentCommandContext, + args: &'a AgentViewArgs, +) -> Result> { + let role_filter = normalized_role_filter(args.role.as_deref()); + let orchestrator_agent_id = context + .effective_config + .agents + .hub + .orchestrator_agent_id + .clone(); + + if !context.effective_config.agents.enabled { + return Ok(StatusReportJson { + disabled: true, + scope_filter: args.scope.as_str(), + role_filter, + orchestrator_agent_id, + sessions: Vec::new(), + nested_llm_records: Vec::new(), + }); + } + + let events = read_trace_agent_events()?; + let mut sessions = BTreeMap::<(String, String), SessionProjection>::new(); + let mut nested = BTreeMap::<(String, String, String), NestedProjection>::new(); + let mut historical_parent_runs = BTreeMap::<(String, String), BTreeSet>::new(); + + for event in events { + let Some(entry) = context.inventory.get(&event.agent_id) else { + continue; + }; + let is_selected_orchestrator = + context.effective_config.agents.hub.orchestrator_agent_id == entry.file.id; + let role = role_for_event( + &event, + &entry.file.id, + is_selected_orchestrator, + &context.effective_config, + ); + let scope = scope_for_event( + &event, + entry, + is_selected_orchestrator, + &context.effective_config, + ); + + if let Some(session_key) = pure_session_key(&event) { + historical_parent_runs + .entry(session_key.clone()) + .or_default() + .insert(event.run_id.clone()); + let orchestration_session_id = session_key.0.clone(); + let world_id = event.world_id.clone(); + let world_generation = event.world_generation; + + let projection = SessionProjection { + last_event_ts: event.ts, + session: StatusSessionJson { + orchestration_session_id: orchestration_session_id.clone(), + agent_id: entry.file.id.clone(), + backend_id: entry.derived_backend_id(), + client: entry.file.id.clone(), + router: PURE_AGENT_ROUTER.to_string(), + protocol: PURE_AGENT_PROTOCOL.to_string(), + execution: ExecutionScopeJson { + scope: scope.as_str(), + }, + role: role.map(ToOwned::to_owned), + last_event_at: event.ts.to_rfc3339(), + world_id: if scope == AgentExecutionScope::World { + world_id.clone() + } else { + None + }, + world_generation: if scope == AgentExecutionScope::World { + world_generation + } else { + None + }, + }, + source: SessionProjectionSource { + orchestration_session_id, + agent_id: entry.file.id.clone(), + run_id: event.run_id.clone(), + ts: event.ts, + is_world_scoped: scope == AgentExecutionScope::World, + has_top_level_world_id: world_id.is_some(), + has_top_level_world_generation: world_generation.is_some(), + }, + }; + + let should_replace = match sessions.get(&session_key) { + Some(existing) => projection.last_event_ts >= existing.last_event_ts, + None => true, + }; + if should_replace { + sessions.insert(session_key, projection); + } + } + + if let Some(projection) = nested_projection(&event, entry) { + nested.insert(projection.sort_key.clone(), projection); + } + } + + let mut filtered_session_projections = Vec::new(); + for projection in sessions.into_values() { + if !matches_scope( + scope_from_label(projection.session.execution.scope), + args.scope, + ) || !matches_role(projection.session.role.as_deref(), role_filter) + { + continue; + } + + if projection.source.is_world_scoped + && (!projection.source.has_top_level_world_id + || !projection.source.has_top_level_world_generation) + { + return Err(config_model::user_error(format!( + "malformed world identity on newest selected world-scoped pure-agent status event: agent_id={} orchestration_session_id={} run_id={} ts={} requires top-level world_id and world_generation", + projection.source.agent_id, + projection.source.orchestration_session_id, + projection.source.run_id, + projection.source.ts.to_rfc3339(), + ))); + } + + filtered_session_projections.push(projection); + } + + let selected_parent_runs: BTreeMap<(String, String), String> = filtered_session_projections + .iter() + .map(|projection| { + ( + ( + projection.source.orchestration_session_id.clone(), + projection.source.agent_id.clone(), + ), + projection.source.run_id.clone(), + ) + }) + .collect(); + + let mut filtered_nested = Vec::new(); + for projection in nested.into_values() { + let parent_key = ( + projection.source.orchestration_session_id.clone(), + projection.source.agent_id.clone(), + ); + let Some(selected_parent_run_id) = selected_parent_runs.get(&parent_key) else { + continue; + }; + let parent_run_id = projection.source.parent_run_id.as_deref(); + let parent_run_matches_selected = parent_run_id == Some(selected_parent_run_id.as_str()); + if parent_run_matches_selected { + let missing_fields = missing_required_nested_fields(&projection); + if !missing_fields.is_empty() { + return Err(config_model::user_error(format!( + "malformed nested tuple on selected status surface: agent_id={} orchestration_session_id={} run_id={} missing_fields={} requires provider and auth_authority on selected nested substrate_gateway status rows", + projection.source.agent_id, + projection.source.orchestration_session_id, + projection.source.run_id, + missing_fields.join(","), + ))); + } + + filtered_nested.push(NestedLlmRecordJson { + parent: NestedParentJson { + orchestration_session_id: projection.source.orchestration_session_id.clone(), + agent_id: projection.source.agent_id.clone(), + }, + run_id: projection.source.run_id.clone(), + backend_id: projection.backend_id, + client: projection.client, + router: NESTED_ROUTER.to_string(), + provider: projection + .provider + .expect("missing required nested provider already validated"), + auth_authority: projection + .auth_authority + .expect("missing required nested auth_authority already validated"), + protocol: projection.protocol, + }); + continue; + } + + let invalid_parent_run_id = format_invalid_parent_run_id(parent_run_id); + let historical_match = parent_run_id.is_some_and(|candidate| { + historical_parent_runs + .get(&parent_key) + .is_some_and(|runs| runs.contains(candidate)) + }); + if historical_match { + continue; + } + + return Err(config_model::user_error(format!( + "malformed nested parent correlation on selected status surface: agent_id={} orchestration_session_id={} run_id={} parent_run_id={} requires parent_run_id to match the winning selected pure-agent run or a known historical pure-agent run for the same session", + projection.source.agent_id, + projection.source.orchestration_session_id, + projection.source.run_id, + invalid_parent_run_id, + ))); + } + + let filtered_sessions: Vec = filtered_session_projections + .into_iter() + .map(|projection| projection.session) + .collect(); + + Ok(StatusReportJson { + disabled: false, + scope_filter: args.scope.as_str(), + role_filter, + orchestrator_agent_id, + sessions: filtered_sessions, + nested_llm_records: filtered_nested, + }) +} + +fn render_status_report(report: &StatusReportJson<'_>, json_mode: bool) -> Result<()> { + if json_mode { + println!("{}", serde_json::to_string_pretty(report)?); + return Ok(()); + } + + println!("disabled: {}", report.disabled); + println!("orchestrator_agent_id: {}", report.orchestrator_agent_id); + println!("scope_filter: {}", report.scope_filter); + if let Some(role_filter) = report.role_filter { + println!("role_filter: {role_filter}"); + } + + println!(); + println!("orchestrator"); + println!(" agent_id: {}", report.orchestrator_agent_id); + + println!(); + println!("sessions"); + for session in &report.sessions { + let mut fields = vec![ + format!( + "orchestration_session_id={}", + session.orchestration_session_id + ), + format!("agent_id={}", session.agent_id), + format!("backend_id={}", session.backend_id), + format!("client={}", session.client), + format!("router={}", session.router), + format!("protocol={}", session.protocol), + format!("execution.scope={}", session.execution.scope), + format!("role={}", session.role.as_deref().unwrap_or("")), + format!("last_event_at={}", session.last_event_at), + ]; + if let (Some(world_id), Some(world_generation)) = + (session.world_id.as_deref(), session.world_generation) + { + fields.push(format!("world_id={world_id}")); + fields.push(format!("world_generation={world_generation}")); + } + println!(" {}", fields.join(" | ")); + } + + if !report.nested_llm_records.is_empty() { + println!(); + println!("nested_llm_records"); + for record in &report.nested_llm_records { + println!( + " parent.orchestration_session_id={} | parent.agent_id={} | run_id={} | backend_id={} | client={} | router={} | provider={} | auth_authority={} | protocol={}", + record.parent.orchestration_session_id, + record.parent.agent_id, + record.run_id, + record.backend_id, + record.client, + record.router, + record.provider, + record.auth_authority, + record.protocol + ); + } + } + + Ok(()) +} + +fn read_trace_agent_events() -> Result> { + let trace_path = trace_log_path()?; + let file = match File::open(&trace_path) { + Ok(file) => file, + Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(err) => { + return Err(err) + .with_context(|| format!("failed to read trace log at {}", trace_path.display())) + } + }; + + let reader = BufReader::new(file); + let mut events = Vec::new(); + for line in reader.lines() { + let line = match line { + Ok(line) => line, + Err(_) => continue, + }; + if line.trim().is_empty() { + continue; + } + let value: serde_json::Value = match serde_json::from_str(&line) { + Ok(value) => value, + Err(_) => continue, + }; + if value.get("event_type").and_then(serde_json::Value::as_str) != Some("agent_event") { + continue; + } + let event: AgentEvent = match serde_json::from_value(value) { + Ok(event) => event, + Err(_) => continue, + }; + events.push(event); + } + + Ok(events) +} + +fn trace_log_path() -> Result { + if let Ok(path) = env::var("SHIM_TRACE_LOG") { + return Ok(PathBuf::from(path)); + } + + Ok(substrate_paths::substrate_home()?.join("trace.jsonl")) +} + +fn pure_session_key(event: &AgentEvent) -> Option<(String, String)> { + let tuple = event.identity_tuple.as_ref()?; + if tuple.router != PURE_AGENT_ROUTER || tuple.protocol != PURE_AGENT_PROTOCOL { + return None; + } + Some(( + event.orchestration_session_id.clone(), + event.agent_id.clone(), + )) +} + +fn nested_projection( + event: &AgentEvent, + entry: &AgentInventoryEntryV1, +) -> Option { + let tuple = event.identity_tuple.as_ref()?; + if tuple.router != NESTED_ROUTER { + return None; + } + let sort_key = ( + event.orchestration_session_id.clone(), + event.agent_id.clone(), + event.run_id.clone(), + ); + + Some(NestedProjection { + sort_key, + source: NestedProjectionSource { + orchestration_session_id: event.orchestration_session_id.clone(), + agent_id: event.agent_id.clone(), + run_id: event.run_id.clone(), + parent_run_id: event.parent_run_id.clone(), + }, + backend_id: entry.derived_backend_id(), + client: event.agent_id.clone(), + protocol: tuple.protocol.clone(), + provider: tuple.provider.clone(), + auth_authority: tuple.auth_authority.clone(), + }) +} + +fn missing_required_nested_fields(projection: &NestedProjection) -> Vec<&'static str> { + let mut missing_fields = Vec::new(); + if projection.provider.is_none() { + missing_fields.push("provider"); + } + if projection.auth_authority.is_none() { + missing_fields.push("auth_authority"); + } + missing_fields +} + +fn format_invalid_parent_run_id(parent_run_id: Option<&str>) -> String { + match parent_run_id { + Some("") => "".to_string(), + Some(value) => value.to_string(), + None => "".to_string(), + } +} + +fn scope_for_event( + event: &AgentEvent, + entry: &AgentInventoryEntryV1, + is_selected_orchestrator: bool, + effective_config: &SubstrateConfig, +) -> AgentExecutionScope { + if is_selected_orchestrator { + return entry.effective_scope(effective_config); + } + + match event + .placement_posture + .as_ref() + .map(|posture| posture.execution) + { + Some(PlacementExecution::HostOnly) => AgentExecutionScope::Host, + Some(PlacementExecution::InWorld) => AgentExecutionScope::World, + None => entry.effective_scope(effective_config), + } +} + +fn role_for_entry<'a>(agent_id: &str, effective_config: &'a SubstrateConfig) -> Option<&'a str> { + if effective_config.agents.hub.orchestrator_agent_id == agent_id { + Some(ORCHESTRATOR_ROLE) + } else { + None + } +} + +fn role_for_event<'a>( + event: &'a AgentEvent, + agent_id: &str, + is_selected_orchestrator: bool, + effective_config: &'a SubstrateConfig, +) -> Option<&'a str> { + if is_selected_orchestrator { + return role_for_entry(agent_id, effective_config); + } + + match event.role.as_deref() { + Some(MEMBER_ROLE) => Some(MEMBER_ROLE), + _ => role_for_entry(agent_id, effective_config), + } +} + +fn eligibility_reason( + entry: &AgentInventoryEntryV1, + effective_config: &SubstrateConfig, + base_policy: &Policy, +) -> Option { + if !entry.file.config.enabled { + return Some("agent is disabled in the effective inventory".to_string()); + } + + let backend_id = entry.derived_backend_id(); + if !backend_allowed(base_policy, &backend_id) { + return Some(format!( + "{backend_id} is not allowlisted by effective policy agents.allowed_backends" + )); + } + + let _ = effective_config; + None +} + +fn backend_allowed(policy: &Policy, backend_id: &str) -> bool { + policy + .agents_allowed_backends + .iter() + .any(|allowed| allowed == backend_id) +} + +fn enabled_world_member_exists( + inventory: &BTreeMap, + effective_config: &SubstrateConfig, +) -> bool { + inventory.values().any(|entry| { + entry.file.config.enabled + && entry.effective_scope(effective_config) == AgentExecutionScope::World + }) +} + +#[derive(Serialize)] +struct DoctorOrchestratorJson<'a> { + agent_id: String, + backend_id: String, + execution: ExecutionScopeJson<'a>, +} + +#[derive(Clone, Serialize)] +struct DoctorCheckJson { + check: String, + status: String, + reason: Option, +} + +#[derive(Serialize)] +struct DoctorReportJson<'a> { + healthy: bool, + fail_closed: bool, + #[serde(skip_serializing_if = "Option::is_none")] + orchestrator: Option>, + checks: Vec, + #[serde(skip)] + world_boundary_exit_code: Option, +} + +enum RequiredWorldBoundaryState { + Ready, + Failed { reason: String, exit_code: i32 }, +} + +fn build_doctor_report(cli: &Cli) -> Result> { + let cwd = current_dir(); + let effective_config = match resolve_effective_config(&cwd, cli) { + Ok(config) => config, + Err(err) => { + if config_model::is_user_error(&err) { + return Ok(failed_doctor_report( + "inventory_scan", + err.to_string(), + None, + )); + } + return Err(err); + } + }; + + let (base_policy, _) = + match substrate_broker::resolve_effective_policy_with_explain(&cwd, false) + .map_err(|err| config_model::user_error(err.to_string())) + { + Ok(value) => value, + Err(err) => { + if config_model::is_user_error(&err) { + return Ok(failed_doctor_report( + "inventory_scan", + err.to_string(), + None, + )); + } + return Err(err); + } + }; + + let inventory = match load_effective_agent_inventory(&cwd, &base_policy) { + Ok(inventory) => inventory, + Err(err) => { + if config_model::is_user_error(&err) { + return Ok(failed_doctor_report( + "inventory_scan", + err.to_string(), + None, + )); + } + return Err(err); + } + }; + + let mut checks = vec![DoctorCheckJson { + check: "inventory_scan".to_string(), + status: "pass".to_string(), + reason: None, + }]; + + let orchestrator = match validate_orchestrator_selection(&effective_config, &inventory) { + Ok(entry) => { + checks.push(DoctorCheckJson { + check: "orchestrator_selection".to_string(), + status: "pass".to_string(), + reason: None, + }); + DoctorOrchestratorJson { + agent_id: entry.file.id.clone(), + backend_id: entry.derived_backend_id(), + execution: ExecutionScopeJson { scope: "host" }, + } + } + Err(reason) => { + checks.push(DoctorCheckJson { + check: "orchestrator_selection".to_string(), + status: "fail".to_string(), + reason: Some(reason), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: None, + checks, + world_boundary_exit_code: None, + }); + } + }; + + if let Some(reason) = policy_allowlist_failure(&effective_config, &inventory, &base_policy) { + checks.push(DoctorCheckJson { + check: "policy_allowlist".to_string(), + status: "fail".to_string(), + reason: Some(reason), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: Some(orchestrator), + checks, + world_boundary_exit_code: None, + }); + } + checks.push(DoctorCheckJson { + check: "policy_allowlist".to_string(), + status: "pass".to_string(), + reason: None, + }); + + if enabled_world_member_exists(&inventory, &effective_config) { + if !effective_config.world.enabled { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "fail".to_string(), + reason: Some( + "world-scoped member posture requires world isolation but world is disabled" + .to_string(), + ), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: Some(orchestrator), + checks, + world_boundary_exit_code: Some(3), + }); + } + + match verify_required_world_boundary(cli) { + RequiredWorldBoundaryState::Ready => { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "pass".to_string(), + reason: None, + }); + } + RequiredWorldBoundaryState::Failed { reason, exit_code } => { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "fail".to_string(), + reason: Some(reason), + }); + return Ok(DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator: Some(orchestrator), + checks, + world_boundary_exit_code: Some(exit_code), + }); + } + } + } else { + checks.push(DoctorCheckJson { + check: "world_boundary".to_string(), + status: "not_applicable".to_string(), + reason: None, + }); + } + + Ok(DoctorReportJson { + healthy: true, + fail_closed: false, + orchestrator: Some(orchestrator), + checks, + world_boundary_exit_code: None, + }) +} + +fn failed_doctor_report( + check: &str, + reason: String, + orchestrator: Option>, +) -> DoctorReportJson<'static> { + DoctorReportJson { + healthy: false, + fail_closed: true, + orchestrator, + checks: vec![DoctorCheckJson { + check: check.to_string(), + status: "fail".to_string(), + reason: Some(reason), + }], + world_boundary_exit_code: None, + } +} + +fn verify_required_world_boundary(cli: &Cli) -> RequiredWorldBoundaryState { + let exe = match env::current_exe() { + Ok(path) => path, + Err(err) => { + return RequiredWorldBoundaryState::Failed { + reason: format!( + "failed to resolve the substrate executable for required world-boundary validation: {err}" + ), + exit_code: 3, + } + } + }; + + let mut command = Command::new(exe); + if cli.world { + command.arg("--world"); + } else if cli.no_world { + command.arg("--no-world"); + } + + let output = match command.args(["world", "doctor", "--json"]).output() { + Ok(output) => output, + Err(err) => { + return RequiredWorldBoundaryState::Failed { + reason: format!( + "failed to run `substrate world doctor --json` for required world-boundary validation: {err}" + ), + exit_code: 3, + } + } + }; + + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let value: serde_json::Value = match serde_json::from_slice(&output.stdout) { + Ok(value) => value, + Err(err) => { + let reason = if stderr.is_empty() { + format!( + "required world-boundary validation returned invalid JSON from `substrate world doctor --json`: {err}" + ) + } else { + format!( + "required world-boundary validation returned invalid JSON from `substrate world doctor --json`: {err}; stderr: {stderr}" + ) + }; + return RequiredWorldBoundaryState::Failed { + reason, + exit_code: 3, + }; + } + }; + + let world_status = value + .pointer("/world/status") + .and_then(serde_json::Value::as_str); + let ok = output.status.success() + && value + .get("ok") + .and_then(serde_json::Value::as_bool) + .unwrap_or(true); + + if ok { + return RequiredWorldBoundaryState::Ready; + } + + let exit_code = classify_world_boundary_exit_code(output.status.code(), world_status); + RequiredWorldBoundaryState::Failed { + reason: format_world_boundary_failure_reason(&value, world_status, &stderr, exit_code), + exit_code, + } +} + +fn classify_world_boundary_exit_code( + process_exit_code: Option, + world_status: Option<&str>, +) -> i32 { + match process_exit_code { + Some(3) => 3, + Some(4) => 4, + _ => match world_status { + Some("unreachable") => 3, + Some("not_provisioned") | Some("missing_prereqs") | Some("unsupported") => 4, + _ => 3, + }, + } +} + +fn format_world_boundary_failure_reason( + value: &serde_json::Value, + world_status: Option<&str>, + stderr: &str, + exit_code: i32, +) -> String { + let summary = match exit_code { + 4 => "required world-scoped member posture is unsupported or not provisioned on this platform/build", + _ => "required world-scoped member boundary is unavailable", + }; + + let detail = value + .get("world_disable_reason") + .and_then(serde_json::Value::as_str) + .or_else(|| { + value + .pointer("/host/world_socket/probe_error") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| { + value + .pointer("/host/error") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| { + value + .pointer("/world/landlock/reason") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| { + value + .pointer("/world/world_fs_strategy/probe/failure_reason") + .and_then(serde_json::Value::as_str) + }) + .or_else(|| (!stderr.is_empty()).then_some(stderr)); + + match (world_status, detail) { + (Some(status), Some(detail)) => format!("{summary} (world.status={status}): {detail}"), + (Some(status), None) => format!("{summary} (world.status={status})"), + (None, Some(detail)) => format!("{summary}: {detail}"), + (None, None) => summary.to_string(), + } +} + +fn validate_orchestrator_selection<'a>( + effective_config: &SubstrateConfig, + inventory: &'a BTreeMap, +) -> std::result::Result<&'a AgentInventoryEntryV1, String> { + if !effective_config.agents.enabled { + return Err("agents are disabled by effective config".to_string()); + } + + let orchestrator_agent_id = effective_config.agents.hub.orchestrator_agent_id.trim(); + if orchestrator_agent_id.is_empty() { + return Err("agents.hub.orchestrator_agent_id must select an orchestrator".to_string()); + } + + let entry = inventory.get(orchestrator_agent_id).ok_or_else(|| { + format!( + "agents.hub.orchestrator_agent_id '{}' is not present in the effective agent inventory", + orchestrator_agent_id + ) + })?; + + if !entry.file.config.enabled { + return Err(format!( + "selected orchestrator '{}' is disabled in the effective inventory", + orchestrator_agent_id + )); + } + + if entry.effective_scope(effective_config) != AgentExecutionScope::Host { + return Err(format!( + "selected orchestrator '{}' must resolve to execution.scope=host", + orchestrator_agent_id + )); + } + + if entry.file.config.protocol.as_deref() != Some(PURE_AGENT_PROTOCOL) { + return Err(format!( + "orchestrator agent '{}' does not advertise protocol '{}'", + orchestrator_agent_id, PURE_AGENT_PROTOCOL + )); + } + + if let Some(capability) = + missing_required_orchestrator_capability(&entry.file.config.capabilities) + { + return Err(format!( + "orchestrator agent '{}' is missing required capability '{}'", + orchestrator_agent_id, capability + )); + } + + Ok(entry) +} + +fn missing_required_orchestrator_capability( + capabilities: &AgentCapabilitiesV1, +) -> Option<&'static str> { + [ + ("session_start", capabilities.session_start), + ("session_resume", capabilities.session_resume), + ("session_fork", capabilities.session_fork), + ("session_stop", capabilities.session_stop), + ("status_snapshot", capabilities.status_snapshot), + ("event_stream", capabilities.event_stream), + ] + .into_iter() + .find_map(|(name, enabled)| (!enabled).then_some(name)) +} + +fn policy_allowlist_failure( + effective_config: &SubstrateConfig, + inventory: &BTreeMap, + base_policy: &Policy, +) -> Option { + let orchestrator = validate_orchestrator_selection(effective_config, inventory).ok()?; + let orchestrator_backend_id = orchestrator.derived_backend_id(); + if !backend_allowed(base_policy, &orchestrator_backend_id) { + return Some(format!( + "selected orchestrator backend '{}' is not allowlisted by effective policy agents.allowed_backends", + orchestrator_backend_id + )); + } + + for entry in inventory.values() { + if !entry.file.config.enabled + || entry.effective_scope(effective_config) != AgentExecutionScope::World + { + continue; + } + let backend_id = entry.derived_backend_id(); + if !backend_allowed(base_policy, &backend_id) { + return Some(format!( + "required world-scoped member backend '{}' is not allowlisted by effective policy agents.allowed_backends", + backend_id + )); + } + } + + None +} + +fn doctor_exit_code(report: &DoctorReportJson<'_>) -> i32 { + if report.healthy { + return 0; + } + + let Some(failed_check) = report + .checks + .iter() + .find(|check| check.status == "fail") + .map(|check| check.check.as_str()) + else { + return 1; + }; + + match failed_check { + "policy_allowlist" => 5, + "world_boundary" => report.world_boundary_exit_code.unwrap_or(3), + "inventory_scan" | "orchestrator_selection" => 2, + _ => 1, + } +} + +fn render_doctor_report(report: &DoctorReportJson<'_>, json_mode: bool) -> Result<()> { + if json_mode { + println!("{}", serde_json::to_string_pretty(report)?); + return Ok(()); + } + + println!( + "{}", + if report.healthy { + "healthy" + } else { + "fail_closed" + } + ); + if let Some(orchestrator) = &report.orchestrator { + println!("orchestrator"); + println!(" agent_id: {}", orchestrator.agent_id); + println!(" backend_id: {}", orchestrator.backend_id); + println!(" execution.scope: {}", orchestrator.execution.scope); + } + println!("checks"); + for check in &report.checks { + match check.reason.as_deref() { + Some(reason) if check.status == "fail" => { + println!(" {}: fail: {}", check.check, reason); + } + _ => println!(" {}: {}", check.check, check.status), + } + } + + Ok(()) +} + +fn matches_scope(scope: AgentExecutionScope, filter: AgentScopeArg) -> bool { + match filter { + AgentScopeArg::Any => true, + AgentScopeArg::Host => scope == AgentExecutionScope::Host, + AgentScopeArg::World => scope == AgentExecutionScope::World, + } +} + +fn scope_from_label(value: &str) -> AgentExecutionScope { + match value { + "host" => AgentExecutionScope::Host, + "world" => AgentExecutionScope::World, + _ => AgentExecutionScope::World, + } +} + +fn matches_role(role: Option<&str>, role_filter: Option<&str>) -> bool { + match role_filter { + Some(filter) => role == Some(filter), + None => true, + } +} + +fn normalized_role_filter(role: Option<&str>) -> Option<&str> { + role.map(str::trim).filter(|role| !role.is_empty()) +} + +fn capabilities_label(summary: &CapabilitiesSummaryJson) -> String { + let mut parts = Vec::new(); + if summary.llm { + parts.push("llm"); + } + if summary.mcp_client { + parts.push("mcp_client"); + } + if parts.is_empty() { + "none".to_string() + } else { + parts.join(",") + } +} + +impl AgentExecutionScope { + fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::World => "world", + } + } +} diff --git a/crates/shell/src/execution/cli.rs b/crates/shell/src/execution/cli.rs index caeac3906..afda96d99 100644 --- a/crates/shell/src/execution/cli.rs +++ b/crates/shell/src/execution/cli.rs @@ -158,6 +158,7 @@ pub enum SubCommands { Config(ConfigCmd), Policy(PolicyCmd), Workspace(WorkspaceCmd), + Agent(AgentCmd), Agents(AgentsCmd), Shim(ShimCmd), Health(HealthCmd), @@ -395,6 +396,66 @@ pub struct PolicySetArgs { pub updates: Vec, } +#[derive(Args, Debug)] +pub struct AgentCmd { + #[command(subcommand)] + pub action: AgentAction, +} + +#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq)] +#[value(rename_all = "snake_case")] +pub enum AgentScopeArg { + Host, + World, + Any, +} + +impl AgentScopeArg { + pub fn as_str(self) -> &'static str { + match self { + Self::Host => "host", + Self::World => "world", + Self::Any => "any", + } + } +} + +impl Default for AgentScopeArg { + fn default() -> Self { + Self::Any + } +} + +#[derive(Args, Debug, Default)] +pub struct AgentViewArgs { + /// Emit JSON instead of human-readable output + #[arg(long)] + pub json: bool, + /// Filter rows by execution scope + #[arg(long, value_name = "host|world|any", default_value = "any")] + pub scope: AgentScopeArg, + /// Filter rows by role label + #[arg(long, value_name = "ROLE")] + pub role: Option, +} + +#[derive(Args, Debug, Default)] +pub struct AgentDoctorArgs { + /// Emit JSON instead of human-readable output + #[arg(long)] + pub json: bool, +} + +#[derive(Subcommand, Debug)] +pub enum AgentAction { + /// List the effective agent inventory + List(AgentViewArgs), + /// Show the current pure-agent and nested gateway-backed status view + Status(AgentViewArgs), + /// Validate deterministic startability of the agent control plane + Doctor(AgentDoctorArgs), +} + #[derive(Args, Debug)] pub struct WorkspaceCmd { #[command(subcommand)] diff --git a/crates/shell/src/execution/invocation/plan.rs b/crates/shell/src/execution/invocation/plan.rs index bf04d650e..c49ed2677 100644 --- a/crates/shell/src/execution/invocation/plan.rs +++ b/crates/shell/src/execution/invocation/plan.rs @@ -6,10 +6,10 @@ use crate::execution::shim_deploy::{DeploymentStatus, ShimDeployer}; #[cfg(target_os = "linux")] use crate::execution::socket_activation; use crate::execution::{ - export_runtime_config_env, handle_agents_command, handle_config_command, handle_graph_command, - handle_health_command, handle_host_command, handle_policy_command, handle_replay_command, - handle_shim_command, handle_trace_command, handle_workspace_command, handle_world_command, - update_world_env, + export_runtime_config_env, handle_agent_command, handle_agents_command, handle_config_command, + handle_graph_command, handle_health_command, handle_host_command, handle_policy_command, + handle_replay_command, handle_shim_command, handle_trace_command, handle_workspace_command, + handle_world_command, update_world_env, }; use anyhow::{Context, Result}; use clap::Parser; @@ -499,6 +499,10 @@ impl ShellConfig { let code = handle_workspace_command(workspace_cmd, &cli); std::process::exit(code); } + SubCommands::Agent(agent_cmd) => { + let code = handle_agent_command(agent_cmd, &cli); + std::process::exit(code); + } SubCommands::Agents(agents_cmd) => { let code = handle_agents_command(agents_cmd, &cli); std::process::exit(code); diff --git a/crates/shell/src/execution/mod.rs b/crates/shell/src/execution/mod.rs index 83c7d1f1c..9a7ead4de 100644 --- a/crates/shell/src/execution/mod.rs +++ b/crates/shell/src/execution/mod.rs @@ -30,6 +30,7 @@ mod value_parse; mod workspace; mod workspace_cmd; +pub(crate) use agents_cmd::handle_agent_command; pub(crate) use agents_cmd::handle_agents_command; pub(crate) use auto_sync::run_auto_sync_if_enabled; pub use cli::*; diff --git a/crates/shell/src/execution/routing/telemetry.rs b/crates/shell/src/execution/routing/telemetry.rs index 6746c66e0..95165b382 100644 --- a/crates/shell/src/execution/routing/telemetry.rs +++ b/crates/shell/src/execution/routing/telemetry.rs @@ -189,7 +189,7 @@ fn append_agent_event_to_trace(config: &ShellConfig, event: &AgentEvent) -> Resu let channel = sanitized.channel.take(); sanitized.set_channel(channel); - let mut entry = serde_json::to_value(&sanitized)?; + let mut entry = sanitized.to_trace_record()?; let obj = entry .as_object_mut() .ok_or_else(|| anyhow::anyhow!("agent event must serialize as a JSON object"))?; diff --git a/crates/shell/src/repl/async_repl.rs b/crates/shell/src/repl/async_repl.rs index 9bd9c488b..ffc4388fc 100644 --- a/crates/shell/src/repl/async_repl.rs +++ b/crates/shell/src/repl/async_repl.rs @@ -1388,7 +1388,9 @@ fn emit_world_restarted_alert( ); event.role = Some("orchestrator".to_string()); event.backend_id = Some("shell:repl".to_string()); - event.world_id = Some(previous_world_id.to_string()); + event.world_id = Some(new_world_id.to_string()); + event.world_generation = Some(new_world_generation); + event.set_pure_agent_telemetry_identity("shell"); if let Some(data) = event.data.as_object_mut() { data.insert("reason".to_string(), serde_json::json!(reason.code())); @@ -1426,6 +1428,8 @@ fn build_world_restart_required_alert( event.role = Some("orchestrator".to_string()); event.backend_id = Some("shell:repl".to_string()); event.world_id = Some(current_world_id.to_string()); + event.world_generation = Some(current_world_generation); + event.set_pure_agent_telemetry_identity("shell"); if let Some(data) = event.data.as_object_mut() { data.insert("reason".to_string(), serde_json::json!(reason.code())); @@ -1434,11 +1438,6 @@ fn build_world_restart_required_alert( serde_json::json!("restart_world"), ); data.insert("on_drift".to_string(), serde_json::json!("fail_closed")); - data.insert("world_id".to_string(), serde_json::json!(current_world_id)); - data.insert( - "world_generation".to_string(), - serde_json::json!(current_world_generation), - ); } event diff --git a/crates/shell/tests/agent_hub_trace_persistence.rs b/crates/shell/tests/agent_hub_trace_persistence.rs index 2327d1f96..504884cff 100644 --- a/crates/shell/tests/agent_hub_trace_persistence.rs +++ b/crates/shell/tests/agent_hub_trace_persistence.rs @@ -11,6 +11,7 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use substrate_common::agent_events::{AgentEvent, MessageEventKind}; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; use support::{binary_path, ensure_substrate_built, temp_dir}; @@ -376,3 +377,38 @@ fn agent_events_append_flattened_agent_event_records_with_join_keys() { ); } } + +#[test] +fn flattened_agent_event_records_retain_parent_run_id_when_present() { + let mut event = AgentEvent::message( + "nested-agent", + "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + MessageEventKind::Status, + "nested gateway request completed", + ); + event.parent_run_id = Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13".to_string()); + + let mut record = event.to_trace_record().expect("flatten agent event"); + let obj = record + .as_object_mut() + .expect("agent event trace record should be an object"); + obj.insert( + "event_type".to_string(), + Value::String("agent_event".to_string()), + ); + obj.insert( + "session_id".to_string(), + Value::String("ses_agent_hub".to_string()), + ); + obj.insert( + "component".to_string(), + Value::String("agent-hub".to_string()), + ); + + assert_eq!( + record.get("parent_run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13"), + "flattened agent_event records must retain parent_run_id when present: {record:?}" + ); +} diff --git a/crates/shell/tests/agent_successor_contract_ahcsitc0.rs b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs new file mode 100644 index 000000000..0f97cb131 --- /dev/null +++ b/crates/shell/tests/agent_successor_contract_ahcsitc0.rs @@ -0,0 +1,2676 @@ +#![cfg(unix)] + +mod common; + +use common::{substrate_shell_driver, temp_dir}; +use serde_json::{json, Value}; +use std::fs; +use std::path::PathBuf; +use std::process::Output; +use tempfile::TempDir; + +const PURE_AGENT_PROTOCOL: &str = "uaa.agent.session"; + +#[derive(Clone, Copy)] +enum CapabilityOverride<'a> { + ForceFalse(&'a str), + Omit(&'a str), +} + +#[derive(Clone, Copy)] +struct SessionContractOptions<'a> { + protocol: Option<&'a str>, + capability_override: Option>, +} + +impl SessionContractOptions<'_> { + const fn default() -> Self { + Self { + protocol: Some(PURE_AGENT_PROTOCOL), + capability_override: None, + } + } +} + +struct AgentSuccessorFixture { + _temp: TempDir, + home: PathBuf, + substrate_home: PathBuf, + workspace_root: PathBuf, +} + +impl AgentSuccessorFixture { + fn new() -> Self { + let temp = temp_dir("substrate-agent-successor-"); + let home = temp.path().join("home"); + fs::create_dir_all(&home).expect("failed to create HOME fixture"); + let substrate_home = temp.path().join("substrate-home"); + fs::create_dir_all(&substrate_home).expect("failed to create SUBSTRATE_HOME fixture"); + let workspace_root = temp.path().join("workspace"); + fs::create_dir_all(&workspace_root).expect("failed to create workspace root"); + Self { + _temp: temp, + home, + substrate_home, + workspace_root, + } + } + + fn command(&self) -> assert_cmd::Command { + let mut cmd = substrate_shell_driver(); + cmd.env("HOME", &self.home) + .env("USERPROFILE", &self.home) + .env("SUBSTRATE_HOME", &self.substrate_home); + cmd + } + + fn init_workspace(&self) { + let output = self + .command() + .arg("workspace") + .arg("init") + .arg(&self.workspace_root) + .arg("--force") + .output() + .expect("failed to run workspace init"); + assert!( + output.status.success(), + "workspace init should succeed: {output:?}" + ); + } + + fn write_global_config_patch(&self, contents: &str) { + fs::write(self.substrate_home.join("config.yaml"), contents) + .expect("failed to write config.yaml"); + } + + fn write_global_policy_patch(&self, contents: &str) { + fs::write(self.substrate_home.join("policy.yaml"), contents) + .expect("failed to write policy.yaml"); + } + + fn write_agent_file(&self, file_name: &str, contents: &str) { + let agents_dir = self.substrate_home.join("agents"); + fs::create_dir_all(&agents_dir).expect("failed to create agents directory"); + fs::write(agents_dir.join(file_name), contents).expect("failed to write agent file"); + } + + fn write_trace_events(&self, events: &[Value]) { + let trace = self.substrate_home.join("trace.jsonl"); + let body = events + .iter() + .map(|event| serde_json::to_string(event).expect("serialize trace event")) + .collect::>() + .join("\n"); + fs::write(trace, format!("{body}\n")).expect("failed to write trace.jsonl"); + } + + fn run(&self, args: &[&str]) -> Output { + self.command() + .current_dir(&self.workspace_root) + .args(args) + .output() + .expect("failed to run substrate command") + } + + fn seed_inventory_for_list_and_status_contracts(&self) { + self.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + self.write_global_policy_patch( + r#"agents: + allowed_backends: + - cli:claude_code + - cli:codex +"#, + ); + self.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + self.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "world", true, false, true), + ); + } + + fn seed_inventory_for_doctor_contract(&self) { + self.seed_doctor_prereqs(); + self.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + self.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + } + + fn seed_doctor_prereqs(&self) { + self.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + self.write_global_policy_patch( + r#"agents: + allowed_backends: + - cli:claude_code + - cli:helper +"#, + ); + } +} + +fn cli_agent_file( + agent_id: &str, + scope: &str, + llm: bool, + mcp_client: bool, + enabled: bool, +) -> String { + cli_agent_file_with_session_contract( + agent_id, + scope, + llm, + mcp_client, + enabled, + SessionContractOptions::default(), + ) +} + +fn cli_agent_file_with_session_contract<'a>( + agent_id: &str, + scope: &str, + llm: bool, + mcp_client: bool, + enabled: bool, + options: SessionContractOptions<'a>, +) -> String { + let mut body = + format!("version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: {enabled}\n"); + if let Some(protocol) = options.protocol { + body.push_str(&format!(" protocol: {protocol}\n")); + } + body.push_str(&format!( + " execution:\n scope: {scope}\n cli:\n binary: {agent_id}\n mode: persistent\n capabilities:\n" + )); + for capability in [ + "session_start", + "session_resume", + "session_fork", + "session_stop", + "status_snapshot", + "event_stream", + ] { + if matches!( + options.capability_override, + Some(CapabilityOverride::Omit(omitted)) if omitted == capability + ) { + continue; + } + let value = !matches!( + options.capability_override, + Some(CapabilityOverride::ForceFalse(false_capability)) + if false_capability == capability + ); + body.push_str(&format!(" {capability}: {value}\n")); + } + body.push_str(&format!(" llm: {llm}\n mcp_client: {mcp_client}\n")); + body +} + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("repo root should exist") +} + +fn read_repo_file(path: &str) -> String { + fs::read_to_string(repo_root().join(path)) + .unwrap_or_else(|err| panic!("failed to read {path}: {err}")) +} + +fn parse_json_output(output: &Output) -> Value { + serde_json::from_slice(&output.stdout).expect("stdout should be valid JSON") +} + +fn assert_malformed_world_identity_failure( + output: &Output, + agent_id: &str, + orchestration_session_id: &str, + run_id: &str, + ts: &str, +) { + assert_eq!( + output.status.code(), + Some(2), + "status should fail closed on malformed world identity: {output:?}" + ); + assert!( + String::from_utf8_lossy(&output.stdout).trim().is_empty(), + "malformed world identity failures should not print stdout: {output:?}" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + for needle in [ + "malformed world identity", + &format!("agent_id={agent_id}"), + &format!("orchestration_session_id={orchestration_session_id}"), + &format!("run_id={run_id}"), + &format!("ts={ts}"), + ] { + assert!( + stderr.contains(needle), + "stderr must contain `{needle}` for malformed world identity failures: {stderr}" + ); + } +} + +fn assert_malformed_nested_parent_correlation_failure( + output: &Output, + agent_id: &str, + orchestration_session_id: &str, + run_id: &str, + parent_run_id: &str, +) { + assert_eq!( + output.status.code(), + Some(2), + "status should fail closed on malformed nested parent correlation: {output:?}" + ); + assert!( + String::from_utf8_lossy(&output.stdout).trim().is_empty(), + "malformed nested parent correlation failures should not print stdout: {output:?}" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + for needle in [ + "malformed nested parent correlation on selected status surface", + &format!("agent_id={agent_id}"), + &format!("orchestration_session_id={orchestration_session_id}"), + &format!("run_id={run_id}"), + &format!("parent_run_id={parent_run_id}"), + ] { + assert!( + stderr.contains(needle), + "stderr must contain `{needle}` for malformed nested parent correlation failures: {stderr}" + ); + } +} + +fn assert_malformed_nested_required_fields_failure( + output: &Output, + agent_id: &str, + orchestration_session_id: &str, + run_id: &str, + missing_fields: &str, +) { + assert_eq!( + output.status.code(), + Some(2), + "status should fail closed on malformed selected nested tuple fields: {output:?}" + ); + assert!( + String::from_utf8_lossy(&output.stdout).trim().is_empty(), + "malformed selected nested tuple field failures should not print stdout: {output:?}" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + for needle in [ + "malformed nested tuple on selected status surface", + &format!("agent_id={agent_id}"), + &format!("orchestration_session_id={orchestration_session_id}"), + &format!("run_id={run_id}"), + &format!("missing_fields={missing_fields}"), + ] { + assert!( + stderr.contains(needle), + "stderr must contain `{needle}` for malformed selected nested tuple field failures: {stderr}" + ); + } +} + +fn assert_doctor_fails_at_orchestrator_selection(output: &Output, expected_reason: &str) { + assert_eq!( + output.status.code(), + Some(2), + "doctor should fail closed at orchestrator_selection: {output:?}" + ); + + let json = parse_json_output(output); + assert_eq!(json.get("healthy").and_then(Value::as_bool), Some(false)); + assert_eq!(json.get("fail_closed").and_then(Value::as_bool), Some(true)); + + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + let observed: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/check") + .and_then(Value::as_str) + .expect("check id should be a string") + }) + .collect(); + assert_eq!( + observed, + vec!["inventory_scan", "orchestrator_selection"], + "doctor must stop before policy_allowlist/world_boundary on orchestrator failures: {json}" + ); + assert_eq!( + checks[1].pointer("/status").and_then(Value::as_str), + Some("fail") + ); + assert_eq!( + checks[1].pointer("/reason").and_then(Value::as_str), + Some(expected_reason), + "doctor must publish the exact orchestrator denial reason: {json}" + ); + assert!( + json.get("orchestrator").is_none() || json["orchestrator"].is_null(), + "failed orchestrator selection must not publish an orchestrator summary: {json}" + ); +} + +fn find_session_by_agent<'a>(sessions: &'a [Value], agent_id: &str) -> &'a Value { + sessions + .iter() + .find(|session| session.pointer("/agent_id").and_then(Value::as_str) == Some(agent_id)) + .unwrap_or_else(|| panic!("expected session row for agent `{agent_id}`")) +} + +fn seed_nested_gateway_status_fixture(fixture: &AgentSuccessorFixture) { + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "world", true, true, true), + ); +} + +#[test] +fn plural_agents_namespace_keeps_validate_as_the_only_compatibility_leaf() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "world", true, false, true), + ); + + let validate = fixture.run(&["agents", "validate"]); + assert!( + validate.status.success(), + "substrate agents validate must remain supported: {validate:?}" + ); + + for alias in [ + ["agents", "list"], + ["agents", "status"], + ["agents", "doctor"], + ] { + let output = fixture.run(&alias); + assert_eq!( + output.status.code(), + Some(2), + "`substrate {}` must remain an invalid plural alias: {output:?}", + alias.join(" ") + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("unrecognized subcommand") + || stderr.contains("unexpected argument") + || stderr.contains("Usage:"), + "plural alias failure should be a CLI usage error\nstderr: {stderr}" + ); + } +} + +#[test] +fn agent_list_json_locks_backend_id_derivation_role_and_omission_rules() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + + let output = fixture.run(&["agent", "list", "--json"]); + assert!( + output.status.success(), + "substrate agent list --json should succeed for a valid successor fixture: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.get("disabled").and_then(Value::as_bool), + Some(false), + "list output must report agents enabled: {json}" + ); + assert_eq!( + json.pointer("/scope_filter").and_then(Value::as_str), + Some("any"), + "default list scope filter must be `any`: {json}" + ); + assert!( + json.pointer("/role_filter") + .is_some_and(serde_json::Value::is_null), + "list output must publish a null role filter when --role is absent: {json}" + ); + + let agents = json["agents"] + .as_array() + .expect("agents should be an array"); + assert_eq!( + agents.len(), + 2, + "fixture should produce exactly two agents: {json}" + ); + + let orchestrator = agents + .iter() + .find(|agent| agent.pointer("/agent_id").and_then(Value::as_str) == Some("claude_code")) + .expect("orchestrator row should exist"); + assert_eq!( + orchestrator.pointer("/backend_id").and_then(Value::as_str), + Some("cli:claude_code"), + "backend_id must be derived as :: {orchestrator}" + ); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "role must remain a separate field from backend_id: {orchestrator}" + ); + assert_eq!( + orchestrator + .pointer("/execution/scope") + .and_then(Value::as_str), + Some("host") + ); + assert_eq!( + orchestrator + .pointer("/capabilities_summary/llm") + .and_then(Value::as_bool), + Some(true) + ); + assert_eq!( + orchestrator + .pointer("/capabilities_summary/mcp_client") + .and_then(Value::as_bool), + Some(true) + ); + assert_eq!( + orchestrator.pointer("/protocol").and_then(Value::as_str), + Some("uaa.agent.session") + ); + + let member = agents + .iter() + .find(|agent| agent.pointer("/agent_id").and_then(Value::as_str) == Some("codex")) + .expect("member row should exist"); + assert_eq!( + member.pointer("/backend_id").and_then(Value::as_str), + Some("cli:codex"), + "member backend_id must be derived as :: {member}" + ); + assert!( + member + .pointer("/role") + .is_some_and(serde_json::Value::is_null), + "non-orchestrator rows must keep role separate and unassigned: {member}" + ); + assert_eq!( + member.pointer("/execution/scope").and_then(Value::as_str), + Some("world") + ); + + for agent in agents { + for forbidden in ["provider", "auth_authority", "world_id", "world_generation"] { + assert!( + agent.get(forbidden).is_none(), + "agent list rows must omit `{forbidden}`: {agent}" + ); + } + } +} + +#[test] +fn agent_status_json_uses_locked_top_level_field_names() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "substrate agent status --json should succeed for a valid successor fixture: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.get("disabled").and_then(Value::as_bool), + Some(false), + "status output must report agents enabled: {json}" + ); + assert_eq!( + json.pointer("/scope_filter").and_then(Value::as_str), + Some("any"), + "default status scope filter must be `any`: {json}" + ); + assert!( + json.pointer("/role_filter") + .is_some_and(serde_json::Value::is_null), + "status output must publish a null role filter when --role is absent: {json}" + ); + assert_eq!( + json.pointer("/orchestrator_agent_id") + .and_then(Value::as_str), + Some("claude_code"), + "status output must report orchestrator_agent_id as the selected inventory id: {json}" + ); + assert!( + json.get("sessions").is_some_and(Value::is_array), + "status output must expose `sessions` as an array: {json}" + ); + assert!( + json.get("nested_llm_records").is_some_and(Value::is_array), + "status output must expose `nested_llm_records` as an array: {json}" + ); +} + +#[test] +fn agent_status_preserves_member_roles_and_filters_them_by_contract_label() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "data": { "message": "orchestrator session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "member session is live" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "unfiltered agent status should succeed for member-role fixtures: {output:?}" + ); + + let json = parse_json_output(&output); + assert!( + json.pointer("/role_filter") + .is_some_and(serde_json::Value::is_null), + "unfiltered status must publish a null role_filter: {json}" + ); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + sessions.len(), + 2, + "unfiltered status should keep both orchestrator and member sessions: {json}" + ); + + let orchestrator = find_session_by_agent(sessions, "claude_code"); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "orchestrator session must preserve the orchestrator role label: {json}" + ); + + let member = find_session_by_agent(sessions, "codex"); + assert_eq!( + member.pointer("/role").and_then(Value::as_str), + Some("member"), + "member session must preserve the member role label: {json}" + ); + assert_eq!( + member.pointer("/world_id").and_then(Value::as_str), + Some("wld_active_0002"), + "world-scoped member status rows must keep world_id: {json}" + ); + assert_eq!( + member.pointer("/world_generation").and_then(Value::as_u64), + Some(7), + "world-scoped member status rows must keep world_generation: {json}" + ); + + let member_output = fixture.run(&["agent", "status", "--role", "member", "--json"]); + assert!( + member_output.status.success(), + "member-filtered agent status should succeed: {member_output:?}" + ); + let member_json = parse_json_output(&member_output); + assert_eq!( + member_json.pointer("/role_filter").and_then(Value::as_str), + Some("member"), + "member-filtered status must report role_filter=member: {member_json}" + ); + let member_sessions = member_json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + member_sessions.len(), + 1, + "--role member should return exactly one member session: {member_json}" + ); + let member_only = find_session_by_agent(member_sessions, "codex"); + assert_eq!( + member_only.pointer("/role").and_then(Value::as_str), + Some("member") + ); + assert_eq!( + member_only.pointer("/world_id").and_then(Value::as_str), + Some("wld_active_0002") + ); + assert_eq!( + member_only + .pointer("/world_generation") + .and_then(Value::as_u64), + Some(7) + ); + + let orchestrator_output = fixture.run(&["agent", "status", "--role", "orchestrator", "--json"]); + assert!( + orchestrator_output.status.success(), + "orchestrator-filtered agent status should succeed: {orchestrator_output:?}" + ); + let orchestrator_json = parse_json_output(&orchestrator_output); + assert_eq!( + orchestrator_json + .pointer("/role_filter") + .and_then(Value::as_str), + Some("orchestrator"), + "orchestrator-filtered status must report role_filter=orchestrator: {orchestrator_json}" + ); + let orchestrator_sessions = orchestrator_json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + orchestrator_sessions.len(), + 1, + "--role orchestrator should return exactly one orchestrator session: {orchestrator_json}" + ); + let orchestrator_only = find_session_by_agent(orchestrator_sessions, "claude_code"); + assert_eq!( + orchestrator_only.pointer("/role").and_then(Value::as_str), + Some("orchestrator") + ); +} + +#[test] +fn agent_status_prefers_newest_pure_session_event_when_trace_lines_are_out_of_order() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + let run_id = "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14"; + let ts = "2026-04-05T00:00:02+00:00"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": orchestration_session_id, + "run_id": run_id, + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "data": { "message": "newest member session event intentionally omits world fields" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_id": "wld_stale_0001", + "world_generation": 6, + "data": { "message": "older stale member session event arrives later in file order" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_world_identity_failure(&output, "codex", orchestration_session_id, run_id, ts); +} + +#[test] +fn agent_status_fails_when_newest_world_scoped_event_omits_top_level_world_id() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + let run_id = "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f16"; + let ts = "2026-04-05T00:00:03+00:00"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:03Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": orchestration_session_id, + "run_id": run_id, + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_generation": 8, + "data": { "message": "newest member session event omits top-level world_id" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "world_id": "wld_stale_0001", + "world_generation": 7, + "data": { "message": "older event remains fully formed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_world_identity_failure(&output, "codex", orchestration_session_id, run_id, ts); +} + +#[test] +fn agent_status_scope_host_ignores_filtered_out_malformed_world_rows() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:03Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f17", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "member", + "data": { "message": "filtered-out world row is malformed" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f18", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "data": { "message": "host-scoped orchestrator row remains valid" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--scope", "host", "--json"]); + assert!( + output.status.success(), + "host scope should ignore malformed world-scoped rows that are filtered out: {output:?}" + ); + + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + sessions.len(), + 1, + "--scope host should only emit the host-scoped session row: {json}" + ); + let session = find_session_by_agent(sessions, "claude_code"); + assert_eq!( + session.pointer("/execution/scope").and_then(Value::as_str), + Some("host") + ); +} + +#[test] +fn agent_status_ignores_non_selected_trace_orchestrator_roles() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "data": { "message": "selected orchestrator session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "stale member row claims orchestrator" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should succeed when a non-selected agent claims orchestrator: {output:?}" + ); + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + + let orchestrator = find_session_by_agent(sessions, "claude_code"); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "selected orchestrator must remain the only orchestrator session: {json}" + ); + + let codex = find_session_by_agent(sessions, "codex"); + assert!( + codex + .pointer("/role") + .is_some_and(serde_json::Value::is_null), + "non-selected stale orchestrator rows must collapse to null role: {json}" + ); + + let orchestrator_output = fixture.run(&["agent", "status", "--role", "orchestrator", "--json"]); + assert!( + orchestrator_output.status.success(), + "orchestrator-filtered status should succeed: {orchestrator_output:?}" + ); + let orchestrator_json = parse_json_output(&orchestrator_output); + let orchestrator_sessions = orchestrator_json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + orchestrator_sessions.len(), + 1, + "only the configured orchestrator should match --role orchestrator: {orchestrator_json}" + ); + assert_eq!( + find_session_by_agent(orchestrator_sessions, "claude_code") + .pointer("/role") + .and_then(Value::as_str), + Some("orchestrator") + ); + + let member_output = fixture.run(&["agent", "status", "--role", "member", "--json"]); + assert!( + member_output.status.success(), + "member-filtered status should succeed: {member_output:?}" + ); + let member_json = parse_json_output(&member_output); + assert_eq!( + member_json.pointer("/role_filter").and_then(Value::as_str), + Some("member"), + "member-filtered status must echo the requested role filter: {member_json}" + ); + assert_eq!( + member_json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "stale non-selected orchestrator roles must not become filterable member rows: {member_json}" + ); +} + +#[test] +fn agent_status_keeps_selected_orchestrator_host_scoped_when_trace_posture_says_in_world() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"agents: + allowed_backends: + - cli:claude_code +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + fixture.write_trace_events(&[json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "placement_posture": { + "execution": "in_world" + }, + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "trace posture should not move the selected orchestrator into world scope" } + })]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should succeed when the selected orchestrator trace posture says in_world: {output:?}" + ); + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + sessions.len(), + 1, + "fixture should project exactly one selected orchestrator session: {json}" + ); + + let orchestrator = &sessions[0]; + assert_eq!( + orchestrator + .pointer("/execution/scope") + .and_then(Value::as_str), + Some("host"), + "selected orchestrator status rows must stay host-scoped despite trace posture: {json}" + ); + assert!( + orchestrator.get("world_id").is_none(), + "host-scoped selected orchestrator rows must omit world_id: {json}" + ); + assert!( + orchestrator.get("world_generation").is_none(), + "host-scoped selected orchestrator rows must omit world_generation: {json}" + ); + + let world_output = fixture.run(&["agent", "status", "--scope", "world", "--json"]); + assert!( + world_output.status.success(), + "world-filtered status should succeed: {world_output:?}" + ); + let world_json = parse_json_output(&world_output); + assert_eq!( + world_json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "world-filtered status must exclude the selected orchestrator row: {world_json}" + ); +} + +#[test] +fn agent_status_unsupported_event_roles_fall_back_to_contract_roles() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_list_and_status_contracts(); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "unexpected", + "data": { "message": "unsupported role should not leak" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "codex", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:codex", + "client": "codex", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "unexpected", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "unsupported role should collapse to null" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should succeed when unsupported event roles are present: {output:?}" + ); + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + + let orchestrator = find_session_by_agent(sessions, "claude_code"); + assert_eq!( + orchestrator.pointer("/role").and_then(Value::as_str), + Some("orchestrator"), + "unsupported explicit roles for the configured orchestrator must fall back to orchestrator: {json}" + ); + + let member = find_session_by_agent(sessions, "codex"); + assert!( + member + .pointer("/role") + .is_some_and(serde_json::Value::is_null), + "unsupported explicit roles for non-orchestrators must fall back to null: {json}" + ); + + let unexpected_output = fixture.run(&["agent", "status", "--role", "unexpected", "--json"]); + assert!( + unexpected_output.status.success(), + "status should not reject unknown role filters even when no rows match: {unexpected_output:?}" + ); + let unexpected_json = parse_json_output(&unexpected_output); + assert_eq!( + unexpected_json + .pointer("/role_filter") + .and_then(Value::as_str), + Some("unexpected"), + "status should echo the requested unexpected role filter: {unexpected_json}" + ); + assert_eq!( + unexpected_json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "unsupported event roles must not become filterable session roles: {unexpected_json}" + ); +} + +#[test] +fn agent_doctor_json_locks_field_names_omissions_and_check_order() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_inventory_for_doctor_contract(); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert!( + output.status.success(), + "substrate agent doctor --json should succeed for a valid host-only successor fixture: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.get("healthy").and_then(Value::as_bool), + Some(true), + "doctor should report a healthy control plane for the host-only fixture: {json}" + ); + assert_eq!( + json.get("fail_closed").and_then(Value::as_bool), + Some(false), + "healthy doctor output must not mark fail_closed: {json}" + ); + assert_eq!( + json.pointer("/orchestrator/agent_id") + .and_then(Value::as_str), + Some("claude_code") + ); + assert_eq!( + json.pointer("/orchestrator/backend_id") + .and_then(Value::as_str), + Some("cli:claude_code"), + "doctor orchestrator summary must publish the derived backend_id: {json}" + ); + assert_eq!( + json.pointer("/orchestrator/execution/scope") + .and_then(Value::as_str), + Some("host"), + "doctor orchestrator summary must preserve execution.scope: {json}" + ); + assert!( + json.pointer("/orchestrator/provider").is_none(), + "doctor orchestrator summary must omit provider: {json}" + ); + assert!( + json.pointer("/orchestrator/auth_authority").is_none(), + "doctor orchestrator summary must omit auth_authority: {json}" + ); + + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + let observed: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/check") + .and_then(Value::as_str) + .expect("check id should be a string") + }) + .collect(); + assert_eq!( + observed, + vec![ + "inventory_scan", + "orchestrator_selection", + "policy_allowlist", + "world_boundary", + ], + "doctor checks must stay in the contract-locked order: {json}" + ); + + let statuses: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/status") + .and_then(Value::as_str) + .expect("check status should be a string") + }) + .collect(); + assert_eq!( + statuses, + vec!["pass", "pass", "pass", "not_applicable"], + "host-only doctor fixture should report a not_applicable world boundary after the three required passes: {json}" + ); +} + +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_protocol_is_missing() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + SessionContractOptions { + protocol: None, + capability_override: None, + }, + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' does not advertise protocol 'uaa.agent.session'", + ); +} + +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_protocol_is_wrong() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + SessionContractOptions { + protocol: Some("openai.responses"), + capability_override: None, + }, + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' does not advertise protocol 'uaa.agent.session'", + ); +} + +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_required_capability_is_false() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + SessionContractOptions { + protocol: Some(PURE_AGENT_PROTOCOL), + capability_override: Some(CapabilityOverride::ForceFalse("event_stream")), + }, + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' is missing required capability 'event_stream'", + ); +} + +#[test] +fn agent_doctor_fails_at_orchestrator_selection_when_required_capability_is_omitted() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.seed_doctor_prereqs(); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file_with_session_contract( + "claude_code", + "host", + true, + true, + true, + SessionContractOptions { + protocol: Some(PURE_AGENT_PROTOCOL), + capability_override: Some(CapabilityOverride::Omit("event_stream")), + }, + ), + ); + fixture.write_agent_file( + "helper.yaml", + &cli_agent_file("helper", "host", false, true, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_doctor_fails_at_orchestrator_selection( + &output, + "orchestrator agent 'claude_code' is missing required capability 'event_stream'", + ); +} + +#[test] +fn docs_usage_and_repo_boundary_match_the_successor_contract() { + let usage = read_repo_file("docs/USAGE.md"); + assert!( + usage.contains("substrate agent list"), + "docs/USAGE.md must document the canonical singular list command" + ); + assert!( + usage.contains("substrate agent status"), + "docs/USAGE.md must document the canonical singular status command" + ); + assert!( + usage.contains("substrate agent doctor"), + "docs/USAGE.md must document the canonical singular doctor command" + ); + for forbidden in [ + "substrate agents list", + "substrate agents status", + "substrate agents doctor", + ] { + assert!( + !usage.contains(forbidden), + "docs/USAGE.md must not advertise plural successor aliases: {forbidden}" + ); + } + + assert!( + !repo_root().join("crates/agent-hub").exists(), + "AHCSITC0 must not introduce a new crates/agent-hub package" + ); +} + +#[test] +fn ahcsitc3_specs_lock_supersession_parity_and_validation_boundaries() { + let compatibility = read_repo_file( + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", + ); + for required in [ + "ADR-0025 is historical evidence only", + "ADR-0025 may be cited only as superseded historical evidence.", + "Existing `agents.allowed_backends` entries remain valid without rewriting because `backend_id` stays derived as `:`.", + "`substrate agents validate` remains supported as an additive compatibility leaf for inventory validation only.", + "`backend_id` remains the agent-side adapter identifier and allowlist token", + ] { + assert!( + compatibility.contains(required), + "compatibility-spec.md must lock AHCSITC3 closeout rule `{required}`" + ); + } + + let parity = read_repo_file( + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", + ); + for required in [ + "| Linux |", + "| macOS |", + "| Windows |", + "`substrate agents validate` remains a compatibility leaf on every platform and never becomes an alias for list, status, or doctor.", + "Nested gateway-backed LLM records always omit `world_id` and `world_generation` on every platform.", + "If the effective command path requires a world-scoped member posture and the required world boundary is temporarily unavailable, `substrate agent doctor` returns exit `3`.", + "If the current build or platform cannot satisfy the required world posture at all, `substrate agent doctor` returns exit `4`.", + "`crates/shell/tests/agents_validate.rs`", + "`crates/shell/tests/agent_hub_trace_persistence.rs`", + "`crates/shell/tests/repl_world_first_routing_v1.rs`", + "`scripts/linux/world-provision.sh`", + "`scripts/mac/lima-warm.sh`", + "`scripts/mac/smoke.sh`", + "`scripts/windows/wsl-warm.ps1`", + "`scripts/windows/wsl-smoke.ps1`", + ] { + assert!( + parity.contains(required), + "platform-parity-spec.md must lock AHCSITC3 parity evidence `{required}`" + ); + } + + let playbook = read_repo_file( + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", + ); + for required in [ + "### Case 1 — `substrate agent list --json` keeps adapter identity and omission rules", + "### Case 2 — `substrate agent status --json` proves a host-scoped orchestrator", + "### Case 3 — world-scoped members publish `world_id` and `world_generation`", + "### Case 4 — nested gateway-backed records publish `run_id`, `provider`, and `auth_authority` on the nested record only", + "### Case 5 — canonical trace keeps the same pure-agent versus nested-record split", + "### Case 6 — `substrate agent doctor --json` proves healthy ordered checks", + "### Case 7 — `substrate agent doctor` fails closed for invalid orchestrator state", + "### Case 8 — `substrate agent doctor` fails closed for world-boundary loss", + "`crates/shell/tests/agents_validate.rs`", + "`crates/shell/tests/agent_hub_trace_persistence.rs`", + "`crates/shell/tests/repl_world_first_routing_v1.rs`", + ] { + assert!( + playbook.contains(required), + "manual_testing_playbook.md must keep AHCSITC3 validation coverage `{required}`" + ); + } +} + +#[test] +fn ahcsitc3_configuration_doc_locks_successor_config_surface() { + let configuration = read_repo_file("docs/CONFIGURATION.md"); + for required in [ + "agents.hub.orchestrator_agent_id", + "agents.allowed_backends", + ] { + assert!( + configuration.contains(required), + "docs/CONFIGURATION.md must document successor config surface `{required}`" + ); + } +} + +#[test] +fn ahcsitc3_trace_doc_locks_tuple_compatible_fields() { + let trace = read_repo_file("docs/TRACE.md"); + for required in [ + "`backend_id`", + "`client`", + "`router`", + "`protocol`", + "`provider`", + "`auth_authority`", + "`world_id`", + "`world_generation`", + ] { + assert!( + trace.contains(required), + "docs/TRACE.md must document tuple-compatible trace field `{required}`" + ); + } +} + +#[test] +fn agent_doctor_fails_closed_on_world_member_allowlist_before_world_boundary() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"world: + enabled: false +agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + fixture.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "world", true, false, true), + ); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_eq!( + output.status.code(), + Some(5), + "doctor should fail on the member backend allowlist before world boundary checks: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!(json.get("healthy").and_then(Value::as_bool), Some(false)); + assert_eq!(json.get("fail_closed").and_then(Value::as_bool), Some(true)); + + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + let observed: Vec<&str> = checks + .iter() + .map(|check| { + check + .pointer("/check") + .and_then(Value::as_str) + .expect("check id should be a string") + }) + .collect(); + assert_eq!( + observed, + vec!["inventory_scan", "orchestrator_selection", "policy_allowlist"], + "fail-closed routing must stop at the member backend allowlist before world_boundary: {json}" + ); + assert_eq!( + checks[2].pointer("/reason").and_then(Value::as_str), + Some( + "required world-scoped member backend 'cli:codex' is not allowlisted by effective policy agents.allowed_backends" + ), + "member dispatch must be gated by the derived backend_id before world boundary handling: {json}" + ); +} + +#[test] +fn agent_doctor_does_not_treat_trace_records_as_control_plane_authorization() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: "" +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_trace_events(&[json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_trace_only", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "data": { "message": "trace says the orchestrator is healthy" } + })]); + + let output = fixture.run(&["agent", "doctor", "--json"]); + assert_eq!( + output.status.code(), + Some(2), + "trace-only observation must not authorize orchestrator selection: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!(json.get("healthy").and_then(Value::as_bool), Some(false)); + let checks = json["checks"] + .as_array() + .expect("checks should be an array"); + assert_eq!( + checks[1].pointer("/check").and_then(Value::as_str), + Some("orchestrator_selection"), + "doctor must still fail at orchestrator selection even when trace records look healthy: {json}" + ); + assert_eq!( + checks[1].pointer("/status").and_then(Value::as_str), + Some("fail") + ); + assert_eq!( + checks[1].pointer("/reason").and_then(Value::as_str), + Some("agents.hub.orchestrator_agent_id must select an orchestrator"), + "event-plane or trace-plane records must not back-authorize control-plane actions: {json}" + ); +} + +#[test] +fn agent_status_uses_top_level_tuple_fields_for_pure_and_nested_records() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "world", true, true, true), + ); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "agent status should stay readable with tuple-compatible trace records: {output:?}" + ); + + let json = parse_json_output(&output); + let sessions = json["sessions"] + .as_array() + .expect("sessions should be an array"); + assert_eq!( + sessions.len(), + 1, + "pure-agent records must still project into the status session surface from top-level tuple fields: {json}" + ); + let session = &sessions[0]; + assert_eq!( + session.pointer("/client").and_then(Value::as_str), + Some("claude_code") + ); + assert_eq!( + session.pointer("/router").and_then(Value::as_str), + Some("agent_hub") + ); + assert_eq!( + session.pointer("/protocol").and_then(Value::as_str), + Some("uaa.agent.session") + ); + assert_eq!( + session.pointer("/world_id").and_then(Value::as_str), + Some("wld_active_0002"), + "world-scoped pure-agent records must publish world_id at the top level: {json}" + ); + assert_eq!( + session.pointer("/world_generation").and_then(Value::as_u64), + Some(7), + "world_generation must stay top-level on world-scoped pure-agent records: {json}" + ); + assert!( + session.get("provider").is_none() && session.get("auth_authority").is_none(), + "pure-agent status sessions must omit nested gateway tuple fields: {session}" + ); + + let nested = json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array"); + assert_eq!( + nested.len(), + 1, + "nested gateway-backed records must remain distinct from pure-agent session rows: {json}" + ); + let record = &nested[0]; + assert_eq!( + record.pointer("/client").and_then(Value::as_str), + Some("claude_code") + ); + assert_eq!( + record.pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14") + ); + assert_eq!( + record.pointer("/router").and_then(Value::as_str), + Some("substrate_gateway") + ); + assert_eq!( + record.pointer("/protocol").and_then(Value::as_str), + Some("openai.responses") + ); + assert_eq!( + record.pointer("/provider").and_then(Value::as_str), + Some("openai") + ); + assert_eq!( + record.pointer("/auth_authority").and_then(Value::as_str), + Some("codex_subscription") + ); + assert!( + record.get("world_id").is_none() && record.get("world_generation").is_none(), + "nested gateway-backed records must omit world scope fields: {record}" + ); +} + +#[test] +fn agent_status_preserves_same_tuple_nested_rows_and_sorts_them_by_run_id() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "world", true, true, true), + ); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "second nested gateway request completed" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "first nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "agent status should preserve multiple same-tuple nested rows in JSON mode: {output:?}" + ); + + let json = parse_json_output(&output); + let nested = json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array"); + assert_eq!( + nested.len(), + 2, + "same-tuple nested gateway records with different run_id values must stay distinct: {json}" + ); + assert_eq!( + nested[0].pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14") + ); + assert_eq!( + nested[1].pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15") + ); + for record in nested { + assert_eq!( + record.pointer("/router").and_then(Value::as_str), + Some("substrate_gateway") + ); + assert_eq!( + record.pointer("/provider").and_then(Value::as_str), + Some("openai") + ); + assert_eq!( + record.pointer("/auth_authority").and_then(Value::as_str), + Some("codex_subscription") + ); + assert_eq!( + record.pointer("/protocol").and_then(Value::as_str), + Some("openai.responses") + ); + assert!( + record.get("world_id").is_none() && record.get("world_generation").is_none(), + "nested gateway-backed records must omit world scope fields: {record}" + ); + } + + let text_output = fixture.run(&["agent", "status"]); + assert!( + text_output.status.success(), + "agent status should preserve multiple same-tuple nested rows in text mode: {text_output:?}" + ); + let stdout = String::from_utf8_lossy(&text_output.stdout); + assert!( + stdout.contains("nested_llm_records"), + "text mode should render a nested_llm_records section when nested records exist\nstdout: {stdout}" + ); + let first_idx = stdout + .find("run_id=0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14") + .expect("text mode should include the lexically first nested run_id"); + let second_idx = stdout + .find("run_id=0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15") + .expect("text mode should include the lexically second nested run_id"); + assert!( + first_idx < second_idx, + "text mode should sort nested rows by run_id rather than insertion order\nstdout: {stdout}" + ); +} + +#[test] +fn agent_status_ignores_stale_nested_rows_from_historical_parent_runs() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0001", + "world_generation": 6, + "data": { "message": "older pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "stale nested gateway request completed" } + }), + json!({ + "ts": "2026-04-05T00:00:02Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "newest pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:03Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f16", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f15", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "current nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert!( + output.status.success(), + "status should ignore stale nested rows tied to historical pure-agent runs: {output:?}" + ); + + let json = parse_json_output(&output); + let nested = json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array"); + assert_eq!( + nested.len(), + 1, + "only nested rows for the winning selected parent run should remain: {json}" + ); + assert_eq!( + nested[0].pointer("/run_id").and_then(Value::as_str), + Some("0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f16") + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_parent_run_id() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_parent_correlation_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_has_empty_parent_run_id() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_parent_correlation_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_has_unknown_parent_run_id() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + let bad_parent_run_id = "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6faa"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": bad_parent_run_id, + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_parent_correlation_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + bad_parent_run_id, + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_provider() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "auth_authority": "codex_subscription", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_required_fields_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "provider", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_auth_authority() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "provider": "openai", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_required_fields_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "auth_authority", + ); +} + +#[test] +fn agent_status_fails_closed_when_selected_nested_row_omits_provider_and_auth_authority() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + let orchestration_session_id = "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12"; + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": orchestration_session_id, + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "parent_run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--json"]); + assert_malformed_nested_required_fields_failure( + &output, + "claude_code", + orchestration_session_id, + "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "provider,auth_authority", + ); +} + +#[test] +fn agent_status_ignores_malformed_nested_rows_when_parent_surface_is_filtered_out() { + let fixture = AgentSuccessorFixture::new(); + seed_nested_gateway_status_fixture(&fixture); + fixture.write_trace_events(&[ + json!({ + "ts": "2026-04-05T00:00:00Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f13", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "agent_hub", + "protocol": "uaa.agent.session", + "role": "orchestrator", + "world_id": "wld_active_0002", + "world_generation": 7, + "data": { "message": "pure-agent session is live" } + }), + json!({ + "ts": "2026-04-05T00:00:01Z", + "event_type": "agent_event", + "session_id": "ses_agent_hub", + "component": "agent-hub", + "kind": "status", + "agent_id": "claude_code", + "orchestration_session_id": "0195f8f1-7a34-7b7f-9c4d-9a7c2f5d6f12", + "run_id": "0195f8f1-7a35-7b7f-9c4d-9a7c2f5d6f14", + "backend_id": "cli:claude_code", + "client": "claude_code", + "router": "substrate_gateway", + "protocol": "openai.responses", + "data": { "summary": "nested gateway request completed" } + }), + ]); + + let output = fixture.run(&["agent", "status", "--role", "member", "--json"]); + assert!( + output.status.success(), + "status should ignore malformed nested rows when the parent pure-agent row is filtered out: {output:?}" + ); + + let json = parse_json_output(&output); + assert_eq!( + json.pointer("/role_filter").and_then(Value::as_str), + Some("member") + ); + assert_eq!( + json["sessions"] + .as_array() + .expect("sessions should be an array") + .len(), + 0, + "filtered-out parent rows should not remain in the selected output surface: {json}" + ); + assert_eq!( + json["nested_llm_records"] + .as_array() + .expect("nested_llm_records should be an array") + .len(), + 0, + "nested rows under filtered-out parents should be ignored rather than failing or emitting output: {json}" + ); +} + +#[test] +fn nested_gateway_policy_does_not_inherit_agent_hub_success_by_implication() { + let fixture = AgentSuccessorFixture::new(); + fixture.init_workspace(); + fixture.write_global_config_patch( + r#"agents: + enabled: true + hub: + orchestrator_agent_id: claude_code +llm: + enabled: true + gateway: + enabled: true + routing: + default_backend: cli:codex +"#, + ); + fixture.write_global_policy_patch( + r#"id: "ahcsitc2-policy" +name: "ahcsitc2-policy" + +world_fs: + host_visible: true + fail_closed: + routing: true + write: + enabled: true + +llm: + allowed_backends: + - "api:openai" + +agents: + allowed_backends: + - "cli:claude_code" + +net_allowed: [] +cmd_allowed: [] +cmd_denied: [] +cmd_isolated: [] + +require_approval: false +allow_shell_operators: true + +limits: + max_memory_mb: null + max_cpu_percent: null + max_runtime_ms: null + max_egress_bytes: null + +metadata: {} +"#, + ); + fixture.write_agent_file( + "claude_code.yaml", + &cli_agent_file("claude_code", "host", true, true, true), + ); + fixture.write_agent_file( + "codex.yaml", + &cli_agent_file("codex", "host", true, false, true), + ); + + let doctor = fixture.run(&["agent", "doctor", "--json"]); + assert!( + doctor.status.success(), + "agent doctor must succeed for the host-scoped orchestrator fixture: {doctor:?}" + ); + let doctor_json = parse_json_output(&doctor); + assert_eq!( + doctor_json.get("healthy").and_then(Value::as_bool), + Some(true) + ); + + let missing_socket = fixture.workspace_root.join("missing-world.sock"); + let output = fixture + .command() + .current_dir(&fixture.workspace_root) + .env_remove("SUBSTRATE_OVERRIDE_WORLD") + .env("SUBSTRATE_WORLD_ENABLED", "1") + .env("SUBSTRATE_WORLD", "enabled") + .env("SUBSTRATE_WORLD_SOCKET", &missing_socket) + .args(["world", "gateway", "status"]) + .output() + .expect("failed to run world gateway status"); + + assert_eq!( + output.status.code(), + Some(5), + "nested gateway approval must remain an independent policy surface after agent-hub success: {output:?}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("cli:codex is not allowlisted by effective policy llm.allowed_backends"), + "nested gateway denial must come from llm.allowed_backends, not implied success: {stderr}" + ); + assert!( + !stderr.contains("required gateway/world component unavailable"), + "gateway allowlist denial must happen before socket/runtime checks: {stderr}" + ); +} diff --git a/crates/shell/tests/agents_validate.rs b/crates/shell/tests/agents_validate.rs index 6a6f2641c..dd0484f09 100644 --- a/crates/shell/tests/agents_validate.rs +++ b/crates/shell/tests/agents_validate.rs @@ -77,6 +77,19 @@ impl AgentsValidateFixture { } fn valid_cli_agent_file(agent_id: &str, policy_overlay: Option<&str>) -> String { + let overlay = policy_overlay.unwrap_or(""); + if overlay.is_empty() { + format!( + "version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: true\n protocol: uaa.agent.session\n execution:\n scope: world\n cli:\n binary: codex\n mode: persistent\n capabilities:\n session_start: true\n session_resume: true\n session_fork: true\n session_stop: true\n status_snapshot: true\n event_stream: true\n" + ) + } else { + format!( + "version: 1\nid: {agent_id}\nconfig:\n kind: cli\n enabled: true\n protocol: uaa.agent.session\n execution:\n scope: world\n cli:\n binary: codex\n mode: persistent\n capabilities:\n session_start: true\n session_resume: true\n session_fork: true\n session_stop: true\n status_snapshot: true\n event_stream: true\npolicy_overlay:\n{overlay}" + ) + } +} + +fn legacy_valid_cli_agent_file(agent_id: &str, policy_overlay: Option<&str>) -> String { let overlay = policy_overlay.unwrap_or(""); if overlay.is_empty() { format!( @@ -114,6 +127,19 @@ world_fs: ); } +#[test] +fn agents_validate_accepts_legacy_inventory_without_protocol_and_session_capabilities() { + let fixture = AgentsValidateFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file("codex.yaml", &legacy_valid_cli_agent_file("codex", None)); + + let output = fixture.validate(); + assert!( + output.status.success(), + "legacy inventory without protocol/session capabilities should still succeed: {output:?}" + ); +} + #[test] fn agents_validate_rejects_unknown_keys_with_exit_2() { let fixture = AgentsValidateFixture::new(); @@ -147,6 +173,88 @@ config: ); } +#[test] +fn agents_validate_rejects_protocol_with_leading_or_trailing_whitespace() { + let fixture = AgentsValidateFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file( + "bad_protocol.yaml", + r#"version: 1 +id: bad_protocol +config: + kind: cli + enabled: true + protocol: " uaa.agent.session " + execution: + scope: world + cli: + binary: codex + mode: persistent + capabilities: + session_start: true + session_resume: true + session_fork: true + session_stop: true + status_snapshot: true + event_stream: true +"#, + ); + + let output = fixture.validate(); + assert_eq!( + output.status.code(), + Some(2), + "whitespace-padded protocol should exit 2: {output:?}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("bad_protocol.yaml") + && stderr.contains("config.protocol must not include leading or trailing whitespace"), + "stderr should mention protocol whitespace rejection\nstderr: {stderr}" + ); +} + +#[test] +fn agents_validate_rejects_invalid_dotted_protocol_with_exit_2() { + let fixture = AgentsValidateFixture::new(); + fixture.init_workspace(); + fixture.write_agent_file( + "bad_protocol.yaml", + r#"version: 1 +id: bad_protocol +config: + kind: cli + enabled: true + protocol: "uaa agent session" + execution: + scope: world + cli: + binary: codex + mode: persistent + capabilities: + session_start: true + session_resume: true + session_fork: true + session_stop: true + status_snapshot: true + event_stream: true +"#, + ); + + let output = fixture.validate(); + assert_eq!( + output.status.code(), + Some(2), + "invalid dotted protocol should exit 2: {output:?}" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("bad_protocol.yaml") + && stderr.contains("config.protocol 'uaa agent session' must be a lowercase dotted id"), + "stderr should mention dotted-id validation\nstderr: {stderr}" + ); +} + #[test] fn agents_validate_rejects_filename_id_mismatch_with_exit_2() { let fixture = AgentsValidateFixture::new(); diff --git a/crates/shell/tests/repl_world_first_routing_v1.rs b/crates/shell/tests/repl_world_first_routing_v1.rs index f97a016b2..fda56ad87 100644 --- a/crates/shell/tests/repl_world_first_routing_v1.rs +++ b/crates/shell/tests/repl_world_first_routing_v1.rs @@ -839,8 +839,13 @@ fn c3_drift_restart_restarts_session_and_emits_message() { ); assert_eq!( alert.get("world_id").and_then(Value::as_str), - Some("wld_stub_0001"), - "top-level world_id must point at the pre-restart world: {alert:?}" + Some("wld_stub_0002"), + "top-level world_id must point at the active replacement world: {alert:?}" + ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(1), + "top-level world_generation must point at the active replacement generation: {alert:?}" ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), @@ -878,6 +883,10 @@ fn c3_drift_restart_restarts_session_and_emits_message() { Some(1), "restart alert must capture new generation 1: {alert:?}" ); + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "restart alerts must keep active world identity at the top level, not duplicate it under data: {alert:?}" + ); } #[test] @@ -938,6 +947,16 @@ fn c3_startup_drift_restart_emits_world_restarted_alert() { ); let alert = alerts[0]; + assert_eq!( + alert.get("world_id").and_then(Value::as_str), + Some("wld_stub_0002"), + "startup restart alert must publish the active replacement world at the top level: {alert:?}" + ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(1), + "startup restart alert must publish the replacement generation at the top level: {alert:?}" + ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), Some("workspace_root_changed"), @@ -969,6 +988,10 @@ fn c3_startup_drift_restart_emits_world_restarted_alert() { Some(1), "startup drift alert must increment to generation 1: {alert:?}" ); + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "startup restart alerts must keep active world identity at the top level, not under data: {alert:?}" + ); } #[test] @@ -1079,6 +1102,11 @@ fn c3_drift_fail_closed_emits_world_restart_required_alert() { Some("wld_stub_0001"), "top-level world_id must point at the current world: {alert:?}" ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(0), + "top-level world_generation must publish the current generation for fail-closed restart-required alerts: {alert:?}" + ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), Some("policy_snapshot_changed"), @@ -1096,17 +1124,9 @@ fn c3_drift_fail_closed_emits_world_restart_required_alert() { Some("restart_world"), "restart-required alert must record required_action=restart_world: {alert:?}" ); - assert_eq!( - alert.pointer("/data/world_id").and_then(Value::as_str), - Some("wld_stub_0001"), - "restart-required alert must capture the current world_id: {alert:?}" - ); - assert_eq!( - alert - .pointer("/data/world_generation") - .and_then(Value::as_u64), - Some(0), - "restart-required alert must capture the current generation: {alert:?}" + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "restart-required alerts must keep current world identity at the top level when known: {alert:?}" ); } @@ -1187,6 +1207,16 @@ fn c3_startup_drift_fail_closed_emits_world_restart_required_alert() { ); let alert = alerts[0]; + assert_eq!( + alert.get("world_id").and_then(Value::as_str), + Some("wld_stub_0001"), + "startup fail-closed alert must publish the current world id at the top level: {alert:?}" + ); + assert_eq!( + alert.get("world_generation").and_then(Value::as_u64), + Some(0), + "startup fail-closed alert must publish the current generation at the top level: {alert:?}" + ); assert_eq!( alert.pointer("/data/reason").and_then(Value::as_str), Some("workspace_root_changed"), @@ -1204,17 +1234,9 @@ fn c3_startup_drift_fail_closed_emits_world_restart_required_alert() { Some("restart_world"), "startup fail-closed alert must record required_action=restart_world: {alert:?}" ); - assert_eq!( - alert.pointer("/data/world_id").and_then(Value::as_str), - Some("wld_stub_0001"), - "startup fail-closed alert must capture the current world id: {alert:?}" - ); - assert_eq!( - alert - .pointer("/data/world_generation") - .and_then(Value::as_u64), - Some(0), - "startup fail-closed alert must capture the current generation: {alert:?}" + assert!( + alert.pointer("/data/world_id").is_none() && alert.pointer("/data/world_generation").is_none(), + "startup fail-closed alerts must keep current world identity at the top level when known: {alert:?}" ); } diff --git a/crates/shell/tests/world_gateway.rs b/crates/shell/tests/world_gateway.rs index 17dd6e77a..5ecdc74ef 100644 --- a/crates/shell/tests/world_gateway.rs +++ b/crates/shell/tests/world_gateway.rs @@ -735,11 +735,17 @@ fn assert_gateway_unavailable(args: &[&str], expected_fragment: &str) { fn assert_gateway_unavailable_json(args: &[&str]) { let mut cmd = substrate_shell_driver(); - cmd.args(args) + let assert = cmd + .args(args) .assert() .code(4) - .stdout("{\"status\":\"unavailable\"}\n") .stderr(predicate::str::is_empty()); + let stdout = + String::from_utf8(assert.get_output().stdout.clone()).expect("gateway status stdout utf8"); + let parsed: JsonValue = + serde_json::from_str(stdout.trim()).expect("parse gateway unavailable json"); + assert_eq!(parsed.pointer("/status"), Some(&json!("unavailable"))); + assert_eq!(parsed.pointer("/client_wiring"), None); } fn gateway_socket_fixture() -> (TempDir, AgentSocket, std::path::PathBuf) { @@ -1212,17 +1218,55 @@ fn world_gateway_status_human_output_omits_missing_optional_fields_without_place #[test] fn world_gateway_disabled_state_skips_typed_runtime_bootstrap() { - let (_temp, _socket, socket_path) = gateway_socket_fixture(); + let (_temp, socket, socket_path) = gateway_socket_fixture(); + let fixture = GatewayAuthFixture::new(); + fixture.write_global_config(gateway_config_with_codex_backend()); + fixture.write_global_agent_inventory("codex.yaml", gateway_inventory_for_codex()); + fixture.write_global_policy(gateway_policy_with_codex_host_credentials()); + fixture.write_codex_auth_state( + r#"{ + "account_id": "acct_disabled_state", + "access_token": "token-disabled-state" +}"#, + ); - let mut cmd = substrate_shell_driver(); - cmd.env("SUBSTRATE_WORLD_ENABLED", "0") + let mut cmd = fixture.command(); + let assert = cmd + .env("SUBSTRATE_WORLD_ENABLED", "0") .env("SUBSTRATE_WORLD", "disabled") .env("SUBSTRATE_WORLD_SOCKET", &socket_path) .args(["world", "gateway", "status", "--json"]) .assert() .code(4) - .stdout("{\"status\":\"unavailable\"}\n") .stderr(predicate::str::is_empty()); + + let stdout = + String::from_utf8(assert.get_output().stdout.clone()).expect("gateway status stdout utf8"); + let parsed: JsonValue = serde_json::from_str(stdout.trim()).expect("parse gateway status json"); + + assert_eq!(parsed.pointer("/status"), Some(&json!("unavailable"))); + assert_eq!(parsed.pointer("/client_wiring"), None); + assert_eq!( + parsed.pointer("/identity_tuple/router"), + Some(&json!("substrate_gateway")) + ); + assert_eq!( + parsed.pointer("/identity_tuple/provider"), + Some(&json!("openai")) + ); + assert_eq!( + parsed.pointer("/identity_tuple/auth_authority"), + Some(&json!("codex_subscription")) + ); + assert_eq!( + parsed.pointer("/placement_posture/execution"), + Some(&json!("in_world")) + ); + assert_eq!( + socket.connection_count(), + 0, + "disabled state must not bootstrap the typed runtime", + ); } #[test] diff --git a/crates/world-agent/src/service.rs b/crates/world-agent/src/service.rs index 6c2d3c328..9417618c9 100644 --- a/crates/world-agent/src/service.rs +++ b/crates/world-agent/src/service.rs @@ -1320,10 +1320,12 @@ impl WorldAgentService { kind: AgentEventKind::Status, orchestration_session_id: span_id.clone(), run_id: span_id.clone(), + parent_run_id: None, backend_id: None, thread_id: None, role: None, world_id: Some(world.id.clone()), + world_generation: None, cmd_id: None, span_id: Some(span_id.clone()), channel: None, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9b173539c..74501b5c3 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -32,6 +32,33 @@ Environment variables and advanced configuration options for Substrate. | `SUBSTRATE_PTY_DEBUG` | Enable PTY debug logging | *none* | `1` | | `SUBSTRATE_PTY_PIPELINE_LAST` | PTY for last pipeline segment | *none* | `1` | +## Agent Hub Configuration + +Agent Hub successor routing is configured through the normal Substrate config and policy files. + +Config keys: +- `agents.hub.orchestrator_agent_id` selects the canonical host-scoped orchestrator agent for `substrate agent status` and `substrate agent doctor`. +- Agent inventory entries continue to define each agent's adapter kind and execution posture; the derived `backend_id` remains `:`. + +Policy keys: +- `agents.allowed_backends` remains the allowlist for derived agent adapter ids such as `cli:codex` or `api:openai`. +- Existing `agents.allowed_backends` entries stay valid across the successor `substrate agent ...` command surface because the policy token is still the derived `backend_id`, not `client`, `router`, `protocol`, `provider`, or `auth_authority`. + +Minimal example: + +```yaml +agents: + hub: + orchestrator_agent_id: claude_code +``` + +```yaml +agents: + allowed_backends: + - cli:claude_code + - cli:codex +``` + ## Manager Manifest & Init | Variable | Purpose | Default | Example | diff --git a/docs/TRACE.md b/docs/TRACE.md index 85fedc967..056204eaa 100644 --- a/docs/TRACE.md +++ b/docs/TRACE.md @@ -149,6 +149,26 @@ These are canonical cross-feature correlation identifiers. Details and required/ - `backend_id`: backend identifier in `:` form (e.g., `cli:codex`, `api:openai`) when a specific backend is involved. - `world_id`: world boundary identity; required on in-world telemetry families (e.g., `world_process_*`) and any record that describes an in-world boundary/session. +### Agent Identity-Tuple Fields + +Agent-hub successor telemetry keeps adapter identity separate from semantic identity: + +- `backend_id`: derived adapter identifier in `:` form. This remains the allowlist and adapter-selection token. +- `client`: pure-agent client identity for successor agent-hub records. +- `router`: routing surface for the record. Pure-agent records use `agent_hub`; nested gateway-backed records use `substrate_gateway`. +- `protocol`: protocol family for the record, such as `uaa.agent.session`. +- `provider`: nested gateway-backed provider identity only; pure-agent records omit it. +- `auth_authority`: nested gateway-backed auth authority only; pure-agent records omit it. +- `parent_run_id`: nested gateway-backed trace correlation only; points at the parent pure-agent `run_id`. +- `world_id`: world boundary identifier for world-scoped pure-agent records and in-world telemetry families. +- `world_generation`: generation counter for the active world-scoped pure-agent session or world-backed execution. + +Operator-facing omission rules: +- Pure-agent records keep `client`, `router`, and `protocol`, and omit `provider` plus `auth_authority`. +- Nested gateway-backed records may add `provider` and `auth_authority`, but they must omit `world_id` and `world_generation`. +- Nested gateway-backed `agent_event` records carry `parent_run_id` in trace; status consumers ignore stale historical nested rows and fail closed only on malformed selected-surface parent correlation. +- Host-scoped pure-agent records omit `world_id` and `world_generation`. + ### Router-Derived Event Families (workflow router daemon; Phase 8) The workflow-router daemon appends derived events to `trace.jsonl` (append-only). v1 uses explicit `event_type` values (see router DR-0016): diff --git a/docs/USAGE.md b/docs/USAGE.md index 1ce7e6b48..f4df596b5 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -59,6 +59,30 @@ The shell includes cross-platform built-ins handled without spawning a child pro through the builtin handler when running non-interactively (`substrate -c`, scripts, etc.). +### Agent Commands + +The successor agent control-plane surface lives under the singular +`substrate agent` namespace: + +```bash +substrate agent list +substrate agent list --json +substrate agent status --scope world +substrate agent doctor --json +``` + +`substrate agents validate` remains available as the inventory-validation +compatibility leaf. It does not alias `substrate agent list`, `status`, or +`doctor`. + +Operator-visible identity rules on these surfaces: +- `backend_id` always renders as `:`. +- Pure-agent list rows omit `provider`, `auth_authority`, `world_id`, and `world_generation`. +- Pure-agent status rows omit `provider` and `auth_authority`. +- `world_id` and `world_generation` render only for world-scoped pure-agent session rows. +- Nested gateway-backed status rows stay separate from pure-agent rows and are the only rows that publish `provider` and `auth_authority`. +- Nested gateway-backed status rows depend on valid trace-side `parent_run_id` correlation; stale historical nested rows are ignored, and malformed selected-surface rows fail closed. + ## PTY Support Substrate automatically uses PTY for interactive commands: diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md index 6d2e740cd..1b1435dec 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/agent-hub-session-protocol-spec.md @@ -189,12 +189,13 @@ An inventory item is orchestrator-eligible only when all of these checks pass: 8. `capabilities.session_stop=true` 9. `capabilities.status_snapshot=true` 10. `capabilities.event_stream=true` -11. the derived `backend_id` is allowed by `agents.allowed_backends` + +After orchestrator eligibility succeeds, control-plane policy evaluation separately checks that the selected derived `backend_id` is allowed by `agents.allowed_backends`. Failure rules: - The hub never falls back to a different orchestrator. - A world-scoped candidate is invalid even when every other capability check passes. -- `substrate agent doctor` and every control-plane entrypoint fail closed on any orchestrator-eligibility failure. +- `substrate agent doctor` and every control-plane entrypoint fail closed on any orchestrator-eligibility or policy-allowlist failure. ## Member dispatch model @@ -303,6 +304,7 @@ This is the exact object shape for each nested record in `substrate agent status "orchestration_session_id": "sess_001", "agent_id": "codex" }, + "run_id": "run_nested_001", "backend_id": "cli:codex", "client": "codex", "router": "substrate_gateway", @@ -314,10 +316,15 @@ This is the exact object shape for each nested record in `substrate agent status Field rules: - `parent.orchestration_session_id` and `parent.agent_id` identify the pure-agent session that triggered the nested request. +- `run_id` is the nested request correlation id and is required on every nested row. - `backend_id` remains the parent agent backend id. - `client` remains the parent agent id. - `router` is exactly `substrate_gateway`. - `provider` and `auth_authority` are required. +- The source nested trace/event record carries `parent_run_id`, but `NestedLlmStatusRecordV1` does not surface it publicly. +- Status emits a nested row only when that source `parent_run_id` matches the winning selected pure-agent `run_id` for the same `(orchestration_session_id, agent_id)` pair. +- Nested rows tied to older historical pure-agent runs for the same pair are ignored as stale history. +- Missing, empty, or otherwise unknown `parent_run_id` on a nested row whose parent pure-agent row is selected causes `substrate agent status` to fail closed. - `world_id` and `world_generation` never appear on `NestedLlmStatusRecordV1`. ### `AgentStatusResponseV1` @@ -341,7 +348,7 @@ Field rules: - `sessions[*]` uses `AgentSessionStatusV1` exactly. - `nested_llm_records[*]` uses `NestedLlmStatusRecordV1` exactly. - `sessions` sort by `orchestration_session_id`, then `agent_id`, ascending byte order. -- `nested_llm_records` sort by `parent.orchestration_session_id`, then `parent.agent_id`, then nested request correlation id ascending byte order. +- `nested_llm_records` sort by `parent.orchestration_session_id`, then `parent.agent_id`, then `run_id` ascending byte order. ## Structured event exchange diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md index 560d6871f..510644ba7 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md @@ -84,7 +84,7 @@ Authoritative inputs: - Stable ordering rules: - inventory rows sort by `agent_id` ascending byte order - session rows sort by `orchestration_session_id`, then `agent_id`, ascending byte order - - nested LLM rows sort by parent `(orchestration_session_id, agent_id)`, then by nested request correlation id once `telemetry-spec.md` names that field + - nested LLM rows sort by parent `(orchestration_session_id, agent_id)`, then by nested `run_id` ascending byte order ### `substrate agent list` @@ -196,14 +196,16 @@ Nested LLM render rules: - Each nested row renders: 1. `parent.orchestration_session_id` 2. `parent.agent_id` - 3. `backend_id` - 4. `client` - 5. `router` - 6. `provider` - 7. `auth_authority` - 8. `protocol` + 3. `run_id` + 4. `backend_id` + 5. `client` + 6. `router` + 7. `provider` + 8. `auth_authority` + 9. `protocol` - `router` renders `substrate_gateway`. - `client` renders the parent agent session's `agent_id`. +- `run_id` renders the nested request correlation id. - `provider` and `auth_authority` are required on every nested row. - Nested rows omit `world_id` and `world_generation`. @@ -238,6 +240,7 @@ Nested LLM render rules: "orchestration_session_id": "sess_001", "agent_id": "codex" }, + "run_id": "run_nested_001", "backend_id": "cli:codex", "client": "codex", "router": "substrate_gateway", @@ -255,6 +258,7 @@ JSON rules: - Each pure-agent session object omits `provider` and `auth_authority`. - `world_id` and `world_generation` are both present or both absent. - `world_generation` is an integer that starts at `0` for a fresh world allocation and increments by `1` on each hub-driven restart of that orchestration session's world. +- Each nested record includes `run_id` as the nested request correlation id. - Each nested record includes `provider` and `auth_authority`. - Each nested record omits `world_id` and `world_generation`. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md index 98872b01f..259a73218 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md @@ -16,7 +16,8 @@ Do not edit planning docs inside the worktree. ## Requirements - Run the advisory CI audit if available, then dispatch compile parity for the checkpoint checkout SHA. - Record run ids, URLs, and any required Linux/macOS/Windows parity follow-ups in `session_log.md`. -- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` currently leaves feature smoke disabled for this checkpoint. +- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` leaves feature smoke disabled for this checkpoint, including Windows. +- Do not dispatch or require `scripts/windows/wsl-smoke.ps1` as part of this checkpoint task. ## End Checklist 1. Confirm compile parity is green or record the blocking failures precisely. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md index 2b0142458..cb9b76714 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md @@ -16,7 +16,8 @@ Do not edit planning docs inside the worktree. ## Requirements - Run the advisory CI audit if available, then dispatch compile parity for the checkpoint checkout SHA. - Record run ids, URLs, and any required Linux/macOS/Windows parity follow-ups in `session_log.md`. -- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` currently leaves feature smoke disabled for this checkpoint. +- Keep the task scoped to compile parity. `pre-planning/ci_checkpoint_plan.md` leaves feature smoke disabled for this checkpoint, including Windows. +- Do not dispatch or require `scripts/windows/wsl-smoke.ps1` as part of this checkpoint task. ## End Checklist 1. Confirm compile parity is green or record the blocking failures precisely. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md index d1e4850bc..e6412770f 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md @@ -49,7 +49,7 @@ Fixture requirements: - the effective inventory contains one host-scoped orchestrator agent selected by `agents.hub.orchestrator_agent_id` - the effective inventory contains at least one host-scoped member and one world-scoped member - the effective policy allowlists the derived orchestrator and member `backend_id` values under `agents.allowed_backends` -- the fixture can trigger one nested gateway-backed request so `nested_llm_records` and correlated trace output are non-empty +- the fixture can trigger at least two nested gateway-backed requests so `nested_llm_records` and correlated trace output are non-empty Evidence capture requirements: @@ -225,9 +225,9 @@ Expected results: - every world-scoped member row includes both `world_id` and `world_generation` - every host-scoped row omits both fields -### Case 4 — nested gateway-backed records publish `provider` and `auth_authority` on the nested record only +### Case 4 — nested gateway-backed records publish `run_id`, `provider`, and `auth_authority` on the nested record only -Trigger one nested gateway-backed request with the feature fixture, then rerun status: +Trigger two nested gateway-backed requests with the feature fixture, then rerun status: ```bash substrate agent status --json | tee "$artifacts_dir/agent-status-nested.json" @@ -238,8 +238,10 @@ Assertions: ```bash jq -e ' (.nested_llm_records | type == "array") and - (length > 0) and + (length > 1) and + ([.nested_llm_records[].run_id] == ([.nested_llm_records[].run_id] | sort)) and all(.nested_llm_records[]; + (.run_id | type == "string") and .router == "substrate_gateway" and (.provider | type == "string") and (.auth_authority | type == "string") and @@ -258,12 +260,29 @@ jq -e ' ' "$artifacts_dir/agent-status-nested.json" ``` +Expected behavior notes: + +- `substrate agent status --json` exits `0` only when nested trace records have valid selected-surface parent correlation. +- Nested rows tied to older historical pure-agent runs for the same `(orchestration_session_id, agent_id)` pair may be absent from `nested_llm_records`; they are ignored as stale history. +- If a nested row would otherwise be selected but its source `parent_run_id` is missing, empty, or unknown for that pair, `substrate agent status` fails closed instead of emitting partial output. + +```bash +substrate agent status | tee "$artifacts_dir/agent-status-nested.txt" +``` + +```bash +rg -n "run_id=" "$artifacts_dir/agent-status-nested.txt" +``` + Expected results: - command exits `0` -- every nested record includes `provider` and `auth_authority` +- every nested record includes `run_id`, `provider`, and `auth_authority` +- nested rows stay distinct even when `router`, `provider`, `auth_authority`, and `protocol` match +- nested rows sort by `run_id` ascending byte order - every pure-agent session row omits `provider` and `auth_authority` - no nested record includes `world_id` or `world_generation` +- text output renders distinct `run_id=` values for nested rows ### Case 5 — canonical trace keeps the same pure-agent versus nested-record split @@ -358,6 +377,27 @@ Expected results: - the first failing check is `orchestrator_selection` - the reason names the exact failing condition from `policy-spec.md` +### Case 7a — `substrate agent doctor` fails closed for policy allowlist denial + +Run this case against two fixture variants: + +- host-scoped orchestrator whose derived `backend_id` is absent from `agents.allowed_backends` +- required world-scoped member whose derived `backend_id` is absent from `agents.allowed_backends` + +Command: + +```bash +substrate agent doctor --json +``` + +Expected results: + +- command exits `5` +- output reports `healthy = false` +- output reports `fail_closed = true` +- the first failing check is `policy_allowlist` +- the reason names the exact failing condition from `policy-spec.md` + ### Case 8 — `substrate agent doctor` fails closed for world-boundary loss Run this case against two fixture variants: @@ -399,7 +439,7 @@ Pass condition: Check: - `agent-hub-session-protocol-spec.md` owns capability descriptors, session handles, lifecycle states, and the machine-readable status objects -- this playbook uses the same `sessions` and `nested_llm_records` object names without widening them +- this playbook uses the same `sessions` and `nested_llm_records` object names and matches the protocol spec's nested `run_id` field - world reuse and restart wording in this playbook matches the protocol spec Pass condition: diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md index b406a4d0a..cd470d330 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md @@ -46,6 +46,7 @@ - `tasks.json` `meta.checkpoint_boundaries` stays `["AHCSITC2", "AHCSITC3"]`. - `tasks.json` `meta.ci_parity_platforms_required` stays `["linux", "macos", "windows"]`. - `tasks.json` `meta.behavior_platforms_required` stays `[]` until a later execution lane adds feature smoke scripts and explicitly widens the behavior-platform requirement. +- Windows stays inside the checkpoint compile-parity set only. Neither `CP1-ci-checkpoint` nor `CP2-ci-checkpoint` requires Windows feature smoke. - `AHCSITC0` and `AHCSITC1` stay normal schema-v4 slices with a single final `*-integ` task. - `AHCSITC2` and `AHCSITC3` use the full boundary model: `*-integ-core`, `*-integ-linux`, `*-integ-macos`, `*-integ-windows`, and final `*-integ`. - The platform-fix tasks for this pack are parity-only follow-up tasks. They do not imply required feature-smoke coverage. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md index a580139f3..14787eea3 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md @@ -110,7 +110,6 @@ These conditions deny before any member dispatch is considered: - `agents.hub.orchestrator_agent_id` is absent - the referenced `agent_id` is missing from the effective inventory - the referenced inventory item is disabled -- the derived orchestrator `backend_id` is denied by `agents.allowed_backends` - `execution.scope = world` - `protocol != uaa.agent.session` - any required orchestrator capability is `false` @@ -120,11 +119,22 @@ The deny explanation must identify the exact failing condition and, when applica - `"effective config agents.hub.orchestrator_agent_id is unset"` - `"orchestrator agent '' is missing from effective inventory"` - `"orchestrator agent '' is disabled in effective inventory"` -- `"orchestrator backend '' is not allowlisted by effective policy agents.allowed_backends"` - `"orchestrator agent '' is world-scoped and host-scoped orchestration is required"` - `"orchestrator agent '' does not advertise protocol 'uaa.agent.session'"` - `"orchestrator agent '' is missing required capability ''"` +### Policy allowlist denial + +These conditions deny after orchestrator selection succeeds and before any world-boundary check or member dispatch is considered: + +- the selected orchestrator derived `backend_id` is denied by `agents.allowed_backends` +- any required world-scoped member derived `backend_id` is denied by `agents.allowed_backends` + +The deny explanation must identify the exact failing condition and the exact policy key: + +- `"selected orchestrator backend '' is not allowlisted by effective policy agents.allowed_backends"` +- `"required world-scoped member backend '' is not allowlisted by effective policy agents.allowed_backends"` + ### Member dispatch denial These conditions deny after orchestrator eligibility succeeds: @@ -202,7 +212,7 @@ These buckets remain distinct in docs, code, and tests. ## Acceptance criteria - Control-plane evaluation uses only the governing inputs named by this spec and rejects event-plane records as authorization inputs. -- Orchestrator eligibility always checks `agents.allowed_backends` before role, protocol, and capability gates. +- `substrate agent doctor` evaluates orchestrator host/protocol/capability selection before the `policy_allowlist` phase that checks derived `backend_id` values. - A world-scoped orchestrator is always denied. - Member dispatch always reapplies `agents.allowed_backends` to the member `backend_id`. - World-scoped member dispatch denies when the world boundary is unavailable under fail-closed posture. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md index 9d44c0c66..ad94b19fc 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md @@ -73,6 +73,7 @@ What risk is reduced by running cross-platform CI here: - Compile parity catches cross-platform build and type drift across `crates/shell`, `crates/agent-api-*`, `crates/common`, and `crates/trace` before the final validation slice starts. - `ci_testing = quick` exercises the high-risk contract and telemetry path without paying full-suite cost before the parity and compatibility closure slice lands. - `feature_smoke = false` in this first pass because the pack defines no feature-local `smoke/` surface and `spec_manifest.md` assigns deterministic validation ownership to `manual_testing_playbook.md`. +- Windows remains part of the compile-parity checkpoint set only. The Windows WSL warm or smoke flow is not a CI checkpoint requirement for `CP1`. Checkpoint-size justification: - This group contains 3 slices, which stays within the machine-readable checkpoint range of 1 to 8 slices. @@ -93,6 +94,7 @@ What risk is reduced by running cross-platform CI here: - Compile parity confirms the final compatibility and parity edits did not reopen cross-platform shell or trace regressions. - `ci_testing = full` provides the last cross-platform confidence gate before planning promotes this pack into execution. - `feature_smoke = false` remains correct in this first pass because the pack still has no feature-local smoke surface. Full planning updates this gate if a smoke workflow becomes part of the accepted validation contract. +- Windows remains part of the compile-parity checkpoint set only. The Windows WSL warm or smoke flow is not a CI checkpoint requirement for `CP2`. Checkpoint-size justification: - This group contains 1 slice, which stays within the machine-readable checkpoint range of 1 to 8 slices. diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md index 718d229a5..819e25015 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md @@ -39,7 +39,8 @@ Strict packs (`tasks.json` → `meta.slice_spec_version >= 2`) requirements: - To compute pack-derived Work Lift v1 from this Touch Set: `make pm-lift-pack PACK="docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` (strict packs only). ### Create -- None +- `crates/common/src/agent_identity.rs` +- `crates/shell/tests/agent_successor_contract_ahcsitc0.rs` ### Edit - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md` @@ -79,16 +80,22 @@ Strict packs (`tasks.json` → `meta.slice_spec_version >= 2`) requirements: - `crates/shell/src/execution/agent_inventory.rs` - `crates/shell/src/execution/agent_events.rs` - `crates/shell/src/execution/routing/telemetry.rs` +- `crates/shell/src/repl/async_repl.rs` - `crates/common/src/agent_events.rs` +- `crates/common/src/lib.rs` - `crates/common/tests/agent_hub_event_envelope_schema.rs` - `crates/trace/src/span.rs` - `crates/trace/src/tests.rs` +- `crates/world-agent/src/service.rs` - `crates/agent-api-types/src/lib.rs` - `crates/agent-api-client/src/lib.rs` - `crates/agent-api-core/src/lib.rs` +- `crates/shell/src/execution/invocation/plan.rs` +- `crates/shell/src/execution/mod.rs` - `crates/shell/tests/agents_validate.rs` - `crates/shell/tests/agent_hub_trace_persistence.rs` - `crates/shell/tests/repl_world_first_routing_v1.rs` +- `crates/shell/tests/world_gateway.rs` ### Deprecate - `docs/project_management/adrs/draft/ADR-0025-agent-hub-core-role-swappable.md` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md index b323e5922..0252e3125 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md @@ -77,3 +77,191 @@ - `NONE` - Next steps: - The pack is execution-ready on the owned planning surfaces. + +## START — 2026-04-24T21:05:00Z — planning — checkpoint scope clarification +- Goal: clarify that Windows remains compile parity only at `CP1` and `CP2` and is not a feature-smoke requirement for CI checkpoints. +- Inputs read: + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md` +- Commands planned: + - `python3 docs/project_management/system/scripts/planning/validate_tasks_json.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` + - `python3 docs/project_management/system/scripts/planning/validate_ci_checkpoint_plan.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` + - `make planning-micro-lint FEATURE_DIR="docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" OWNED_PATHS="plan.md tasks.json session_log.md kickoff_prompts/CP1-ci-checkpoint.md kickoff_prompts/CP2-ci-checkpoint.md pre-planning/ci_checkpoint_plan.md"` + +## END — 2026-04-24T21:05:00Z — planning — checkpoint scope clarification +- Summary of changes: + - Updated `plan.md` to state explicitly that Windows stays in the CI compile-parity set only and is not a checkpoint feature-smoke requirement. + - Updated `pre-planning/ci_checkpoint_plan.md` so both `CP1` and `CP2` call out that Windows WSL warm or smoke flows are outside checkpoint scope. + - Updated checkpoint kickoff prompts and `tasks.json` acceptance criteria or checklists so operators do not dispatch Windows feature smoke during checkpoint execution. +- Files created or modified: + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md` + - `docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md` +- Validation commands run (with results): + - `python3 docs/project_management/system/scripts/planning/validate_tasks_json.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` → `0` → `PASS` + - `python3 docs/project_management/system/scripts/planning/validate_ci_checkpoint_plan.py --feature-dir "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible"` → `0` → `PASS` + - `make planning-micro-lint FEATURE_DIR="docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" OWNED_PATHS="plan.md tasks.json session_log.md kickoff_prompts/CP1-ci-checkpoint.md kickoff_prompts/CP2-ci-checkpoint.md pre-planning/ci_checkpoint_plan.md"` → `0` → `PASS` +- Blockers: + - `NONE` +- Next steps: + - Use the checkpoint tasks as compile-parity-only gates unless a later planning change explicitly introduces a feature-local smoke surface. + +## START — 2026-04-25T01:19:27Z — code — AHCSITC0-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC0"` + +## START — 2026-04-25T01:19:27Z — test — AHCSITC0-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC0"` + +## END — 2026-04-25T01:35:24Z — code — AHCSITC0-code +- HEAD: `e8801afac6f568a3855c158d6547cebcb18663b4` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/code/last_message.md` + +## END — 2026-04-25T01:35:24Z — test — AHCSITC0-test +- HEAD: `c8d7c9488066305762d8d4c000c3f3835c0c3207` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/test/last_message.md` + +## START — 2026-04-25T01:35:24Z — integration — AHCSITC0-integ +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC0-integ" LAUNCH_CODEX=1` + +## END — 2026-04-25T01:49:57Z — integration — AHCSITC0-integ +- HEAD: `60ce0818f76ede1b002726c2daa2715258a1d0d0` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC0/integ/last_message.md` + +## START — 2026-04-25T03:28:54Z — code — AHCSITC1-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC1"` + +## START — 2026-04-25T03:28:54Z — test — AHCSITC1-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC1"` + +## END — 2026-04-25T03:38:04Z — code — AHCSITC1-code +- HEAD: `c21ae91091bddf69b67f71680316df9ce0c3a650` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/code/last_message.md` + +## END — 2026-04-25T03:38:04Z — test — AHCSITC1-test +- HEAD: `c21ae91091bddf69b67f71680316df9ce0c3a650` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/test/last_message.md` + +## START — 2026-04-25T03:38:04Z — integration — AHCSITC1-integ +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC1-integ" LAUNCH_CODEX=1` + +## END — 2026-04-25T03:46:35Z — integration — AHCSITC1-integ +- HEAD: `45864b834aae0275c4cb060be8d46ddc46093d8b` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC1/integ/last_message.md` + +## START — 2026-04-25T03:50:28Z — code — AHCSITC2-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC2"` + +## START — 2026-04-25T03:50:28Z — test — AHCSITC2-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC2"` + +## END — 2026-04-25T04:07:55Z — code — AHCSITC2-code +- HEAD: `66c56b74362470d7b0891a7692cf1fca1b28b263` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/code/last_message.md` + +## END — 2026-04-25T04:07:55Z — test — AHCSITC2-test +- HEAD: `b7ec0f55b3630e5695019825cfb8996f1e2482e0` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/test/last_message.md` + +## START — 2026-04-25T04:07:55Z — integration — AHCSITC2-integ-core +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC2-integ-core" LAUNCH_CODEX=1` + +## END — 2026-04-25T04:26:12Z — integration — AHCSITC2-integ-core +- HEAD: `5d8644570b17903550c7992eb931aa9adbec048c` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/integ-core/last_message.md` + +## START — 2026-04-25T04:27:47Z — checkpoint — CP1-ci-checkpoint +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="CP1-ci-checkpoint"` + +## END — 2026-04-25T04:30:34Z — checkpoint — CP1-ci-checkpoint +- Candidate SHA: `5d8644570b17903550c7992eb931aa9adbec048c` +- Advisory CI audit: + - `scripts/ci-audit/ci_audit.sh --ledger-path "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/ci-audit/ledger.jsonl" --kind ci-testing --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --head-sha "5d8644570b17903550c7992eb931aa9adbec048c"` → `RECOMMEND=run` (`REASON=no_last_green_run_found`) +- Compile parity: + - `make ci-compile-parity CI_WORKFLOW_REF="feat/agent-hub-core-successor-identity-tuple-compatible" CI_REMOTE=origin CI_CLEANUP=1 CI_CHECKOUT_REF="5d8644570b17903550c7992eb931aa9adbec048c"` → `RUN_ID=24922568397` → `RUN_URL=https://github.com/atomize-hq/substrate/actions/runs/24922568397` → `CONCLUSION=success` + - Passed OSes: `macos-14,ubuntu-24.04,windows-2022` + - Failed OSes: `NONE` + - Failed jobs: `NONE` +- CI audit ledger: + - `scripts/ci-audit/ci_audit_record.sh --ledger-path "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/ci-audit/ledger.jsonl" --kind ci-testing --mode compile-parity --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --run-id "24922568397" --tested-sha "5d8644570b17903550c7992eb931aa9adbec048c"` → `RECORDED=1` +- Feature smoke: + - Not dispatched. `pre-planning/ci_checkpoint_plan.md` and `CP1-ci-checkpoint.md` keep `feature_smoke=false` for this checkpoint, including Windows. + +## START — 2026-04-25T04:33:55Z — integration-final — AHCSITC2-integ +- Dispatch: + - `make triad-task-start-integ-final FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC2" LAUNCH_CODEX=1` + +## END — 2026-04-25T04:43:26Z — integration-final — AHCSITC2-integ +- HEAD: `61cdc894150b0d9bc197ce5bdf1b211b7e01bda0` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC2/integ/last_message.md` +- Merge result: + - `make triad-task-finish TASK_ID="AHCSITC2-integ"` → `MERGED_TO_ORCH=true` + +## START — 2026-04-25T10:58:56Z — code — AHCSITC3-code +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC3"` + +## START — 2026-04-25T10:58:56Z — test — AHCSITC3-test +- Dispatch: + - `make triad-task-start-complete FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC3"` + +## END — 2026-04-25T11:09:49Z — code — AHCSITC3-code +- HEAD: `9a63080ef24f49e16b1368f2e741b0f40e9047b3` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/code/last_message.md` + +## END — 2026-04-25T11:09:49Z — test — AHCSITC3-test +- HEAD: `19b972a20b747098ddd9df2bc503c60fadfdeb47` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/test/last_message.md` + +## START — 2026-04-25T11:09:49Z — integration — AHCSITC3-integ-core +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="AHCSITC3-integ-core" LAUNCH_CODEX=1` + +## END — 2026-04-25T11:21:48Z — integration — AHCSITC3-integ-core +- HEAD: `c6466f0cc2eeed9f9c62f285ea82ad8d41313452` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/integ-core/last_message.md` + +## START — 2026-04-25T11:23:11Z — checkpoint — CP2-ci-checkpoint +- Dispatch: + - `make triad-task-start FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" TASK_ID="CP2-ci-checkpoint"` + +## END — 2026-04-25T11:27:39Z — checkpoint — CP2-ci-checkpoint +- Candidate SHA: `c6466f0cc2eeed9f9c62f285ea82ad8d41313452` +- Advisory CI audit: + - `scripts/ci-audit/ci_audit.sh --ledger-path "/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/ci-audit/ledger.jsonl" --kind ci-testing --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --head-sha "c6466f0cc2eeed9f9c62f285ea82ad8d41313452"` → `RECOMMEND=run` (`REASON=changes_since_last_green`) +- Compile parity: + - `make ci-compile-parity CI_WORKFLOW_REF="feat/agent-hub-core-successor-identity-tuple-compatible" CI_REMOTE=origin CI_CLEANUP=1 CI_CHECKOUT_REF="c6466f0cc2eeed9f9c62f285ea82ad8d41313452"` → `RUN_ID=24929777193` → `RUN_URL=https://github.com/atomize-hq/substrate/actions/runs/24929777193` → `CONCLUSION=success` + - Passed OSes: `macos-14,ubuntu-24.04,windows-2022` + - Failed OSes: `NONE` + - Failed jobs: `NONE` +- CI audit ledger: + - `scripts/ci-audit/ci_audit_record.sh --ledger-path "/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/ci-audit/ledger.jsonl" --kind ci-testing --mode compile-parity --orch-branch "feat/agent-hub-core-successor-identity-tuple-compatible" --run-id "24929777193" --tested-sha "c6466f0cc2eeed9f9c62f285ea82ad8d41313452"` → `RECORDED=1` +- Feature smoke: + - Not dispatched. `pre-planning/ci_checkpoint_plan.md` and `CP2-ci-checkpoint.md` keep `feature_smoke=false` for this checkpoint, including Windows. + +## START — 2026-04-25T11:28:10Z — integration-final — AHCSITC3-integ +- Dispatch: + - `make triad-task-start-integ-final FEATURE_DIR="/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible" SLICE_ID="AHCSITC3" LAUNCH_CODEX=1` + +## END — 2026-04-25T11:37:10Z — integration-final — AHCSITC3-integ +- HEAD: `25c7b3448c7131c66d861273bacefbb86520ee69` +- Codex last message: `/home/spenser/__Active_code/substrate/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/logs/AHCSITC3/integ/last_message.md` +- Merge result: + - `make triad-task-finish TASK_ID="AHCSITC3-integ"` → `MERGED_TO_ORCH=true` diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json index ddbf6ab86..f05565a29 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json @@ -1,49 +1,30 @@ { "meta": { - "schema_version": 4, - "slice_spec_version": 2, - "feature": "agent-hub-core-successor-identity-tuple-compatible", - "cross_platform": true, - "execution_gates": false, + "adr_paths": [ + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" + ], "automation": { "enabled": true, "orchestration_branch": "feat/agent-hub-core-successor-identity-tuple-compatible" }, "behavior_platforms_required": [], + "checkpoint_boundaries": [ + "AHCSITC2", + "AHCSITC3" + ], "ci_parity_platforms_required": [ "linux", "macos", "windows" ], - "checkpoint_boundaries": [ - "AHCSITC2", - "AHCSITC3" - ], - "adr_paths": [ - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" - ] + "cross_platform": true, + "execution_gates": false, + "feature": "agent-hub-core-successor-identity-tuple-compatible", + "schema_version": 4, + "slice_spec_version": 2 }, "tasks": [ { - "id": "AHCSITC0-code", - "name": "AHCSITC0 slice (code)", - "type": "code", - "phase": "AHCSITC0", - "status": "pending", - "description": "Implement the AHCSITC0 operator contract and identity presentation behaviors.", - "references": [ - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/AHCSITC0-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" - ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." - ], "ac_ids": [ "AC-AHCSITC0-01", "AC-AHCSITC0-02", @@ -52,37 +33,26 @@ "AC-AHCSITC0-05", "AC-AHCSITC0-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." + ], + "concurrent_with": [ + "AHCSITC0-test" ], + "depends_on": [], + "description": "Implement the AHCSITC0 operator contract and identity presentation behaviors.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC0-code\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code", + "id": "AHCSITC0-code", "integration_task": "AHCSITC0-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/kickoff_prompts/AHCSITC0-code.md", - "depends_on": [], - "concurrent_with": [ - "AHCSITC0-test" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { - "id": "AHCSITC0-test", - "name": "AHCSITC0 slice (test)", - "type": "test", + "name": "AHCSITC0 slice (code)", "phase": "AHCSITC0", - "status": "pending", - "description": "Add tests that enforce the AHCSITC0 operator contract and identity presentation behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -93,9 +63,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" ], + "status": "completed", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-code" + }, + { "ac_ids": [ "AC-AHCSITC0-01", "AC-AHCSITC0-02", @@ -104,37 +85,26 @@ "AC-AHCSITC0-05", "AC-AHCSITC0-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC0/AHCSITC0-spec.md)." + ], + "concurrent_with": [ + "AHCSITC0-code" ], + "depends_on": [], + "description": "Add tests that enforce the AHCSITC0 operator contract and identity presentation behaviors.", "end_checklist": [ "cargo fmt", "Run the targeted tests you add or touch.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC0-test\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test", + "id": "AHCSITC0-test", "integration_task": "AHCSITC0-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/kickoff_prompts/AHCSITC0-test.md", - "depends_on": [], - "concurrent_with": [ - "AHCSITC0-code" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test", - "required_make_targets": [ - "triad-test-checks" - ] - }, - { - "id": "AHCSITC0-integ", - "name": "AHCSITC0 slice (integration final)", - "type": "integration", + "name": "AHCSITC0 slice (test)", "phase": "AHCSITC0", - "status": "pending", - "description": "Finalize AHCSITC0 after the code and test branches merge cleanly.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -143,11 +113,22 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Slice is green under make integ-checks and implements the behaviors required by ac_ids." + "required_make_targets": [ + "triad-test-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC0\"" ], + "status": "completed", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-test" + }, + { "ac_ids": [ "AC-AHCSITC0-01", "AC-AHCSITC0-02", @@ -156,12 +137,15 @@ "AC-AHCSITC0-05", "AC-AHCSITC0-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC0-integ\"" + "acceptance_criteria": [ + "Slice is green under make integ-checks and implements the behaviors required by ac_ids." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC0-code", + "AHCSITC0-test" ], + "description": "Finalize AHCSITC0 after the code and test branches merge cleanly.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", @@ -170,27 +154,67 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC0-integ\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ", + "id": "AHCSITC0-integ", "integration_task": "AHCSITC0-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/kickoff_prompts/AHCSITC0-integ.md", - "depends_on": [ - "AHCSITC0-code", - "AHCSITC0-test" + "merge_to_orchestration": true, + "name": "AHCSITC0 slice (integration final)", + "phase": "AHCSITC0", + "references": [ + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC0/AHCSITC0-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/contract.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ", "required_make_targets": [ "integ-checks" ], - "merge_to_orchestration": true + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC0-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC0-integ\"" + ], + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc0-integ" }, { + "ac_ids": [ + "AC-AHCSITC1-01", + "AC-AHCSITC1-02", + "AC-AHCSITC1-03", + "AC-AHCSITC1-04", + "AC-AHCSITC1-05", + "AC-AHCSITC1-06" + ], + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." + ], + "concurrent_with": [ + "AHCSITC1-test" + ], + "depends_on": [ + "AHCSITC0-integ" + ], + "description": "Implement the AHCSITC1 session protocol and placement behaviors.", + "end_checklist": [ + "cargo fmt", + "cargo clippy --workspace --all-targets -- -D warnings", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-code\"", + "Update tasks.json and session_log.md on the orchestration branch." + ], + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code", "id": "AHCSITC1-code", + "integration_task": "AHCSITC1-integ", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-code.md", "name": "AHCSITC1 slice (code)", - "type": "code", "phase": "AHCSITC1", - "status": "pending", - "description": "Implement the AHCSITC1 session protocol and placement behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -201,9 +225,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], + "status": "completed", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code" + }, + { "ac_ids": [ "AC-AHCSITC1-01", "AC-AHCSITC1-02", @@ -212,39 +247,28 @@ "AC-AHCSITC1-05", "AC-AHCSITC1-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." ], - "end_checklist": [ - "cargo fmt", - "cargo clippy --workspace --all-targets -- -D warnings", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-code\"", - "Update tasks.json and session_log.md on the orchestration branch." + "concurrent_with": [ + "AHCSITC1-code" ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code", - "integration_task": "AHCSITC1-integ", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-code.md", "depends_on": [ "AHCSITC0-integ" ], - "concurrent_with": [ - "AHCSITC1-test" + "description": "Add tests that enforce the AHCSITC1 session protocol and placement behaviors.", + "end_checklist": [ + "cargo fmt", + "Run the targeted tests you add or touch.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-test\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test", "id": "AHCSITC1-test", + "integration_task": "AHCSITC1-integ", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-test.md", "name": "AHCSITC1 slice (test)", - "type": "test", "phase": "AHCSITC1", - "status": "pending", - "description": "Add tests that enforce the AHCSITC1 session protocol and placement behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -255,9 +279,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC1/AHCSITC1-spec.md)." + "required_make_targets": [ + "triad-test-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" ], + "status": "completed", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test" + }, + { "ac_ids": [ "AC-AHCSITC1-01", "AC-AHCSITC1-02", @@ -266,39 +301,30 @@ "AC-AHCSITC1-05", "AC-AHCSITC1-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC1-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC1\"" + "acceptance_criteria": [ + "Slice is green under make integ-checks and implements the behaviors required by ac_ids." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC1-code", + "AHCSITC1-test" ], + "description": "Finalize AHCSITC1 after the code and test branches merge cleanly.", "end_checklist": [ "cargo fmt", - "Run the targeted tests you add or touch.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-test\"", + "cargo clippy --workspace --all-targets -- -D warnings", + "Run relevant tests.", + "make integ-checks", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-integ\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test", - "integration_task": "AHCSITC1-integ", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-test.md", - "depends_on": [ - "AHCSITC0-integ" - ], - "concurrent_with": [ - "AHCSITC1-code" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-test", - "required_make_targets": [ - "triad-test-checks" - ] - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ", "id": "AHCSITC1-integ", + "integration_task": "AHCSITC1-integ", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-integ.md", + "merge_to_orchestration": true, "name": "AHCSITC1 slice (integration final)", - "type": "integration", "phase": "AHCSITC1", - "status": "pending", - "description": "Finalize AHCSITC1 after the code and test branches merge cleanly.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -309,16 +335,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "Slice is green under make integ-checks and implements the behaviors required by ac_ids." - ], - "ac_ids": [ - "AC-AHCSITC1-01", - "AC-AHCSITC1-02", - "AC-AHCSITC1-03", - "AC-AHCSITC1-04", - "AC-AHCSITC1-05", - "AC-AHCSITC1-06" + "required_make_targets": [ + "integ-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", @@ -326,35 +344,41 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC1-integ\"" ], + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ" + }, + { + "ac_ids": [ + "AC-AHCSITC2-01", + "AC-AHCSITC2-02", + "AC-AHCSITC2-03", + "AC-AHCSITC2-04", + "AC-AHCSITC2-05", + "AC-AHCSITC2-06" + ], + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." + ], + "concurrent_with": [ + "AHCSITC2-test" + ], + "depends_on": [ + "AHCSITC1-integ" + ], + "description": "Implement the AHCSITC2 fail-closed policy and telemetry publication behaviors.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", - "Run relevant tests.", - "make integ-checks", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC1-integ\"", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-code\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ", - "integration_task": "AHCSITC1-integ", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC1/kickoff_prompts/AHCSITC1-integ.md", - "depends_on": [ - "AHCSITC1-code", - "AHCSITC1-test" - ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc1-integ", - "required_make_targets": [ - "integ-checks" - ], - "merge_to_orchestration": true - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code", "id": "AHCSITC2-code", + "integration_task": "AHCSITC2-integ-core", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-code.md", "name": "AHCSITC2 slice (code)", - "type": "code", "phase": "AHCSITC2", - "status": "pending", - "description": "Implement the AHCSITC2 fail-closed policy and telemetry publication behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -365,9 +389,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" ], + "status": "completed", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code" + }, + { "ac_ids": [ "AC-AHCSITC2-01", "AC-AHCSITC2-02", @@ -376,39 +411,28 @@ "AC-AHCSITC2-05", "AC-AHCSITC2-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." ], - "end_checklist": [ - "cargo fmt", - "cargo clippy --workspace --all-targets -- -D warnings", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-code\"", - "Update tasks.json and session_log.md on the orchestration branch." + "concurrent_with": [ + "AHCSITC2-code" ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code", - "integration_task": "AHCSITC2-integ-core", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-code.md", "depends_on": [ "AHCSITC1-integ" ], - "concurrent_with": [ - "AHCSITC2-test" + "description": "Add tests that enforce the AHCSITC2 fail-closed policy and telemetry publication behaviors.", + "end_checklist": [ + "cargo fmt", + "Run the targeted tests you add or touch.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-test\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test", "id": "AHCSITC2-test", + "integration_task": "AHCSITC2-integ-core", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-test.md", "name": "AHCSITC2 slice (test)", - "type": "test", "phase": "AHCSITC2", - "status": "pending", - "description": "Add tests that enforce the AHCSITC2 fail-closed policy and telemetry publication behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -419,16 +443,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/spec_manifest.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC2/AHCSITC2-spec.md)." - ], - "ac_ids": [ - "AC-AHCSITC2-01", - "AC-AHCSITC2-02", - "AC-AHCSITC2-03", - "AC-AHCSITC2-04", - "AC-AHCSITC2-05", - "AC-AHCSITC2-06" + "required_make_targets": [ + "triad-test-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", @@ -436,33 +452,37 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC2\"" ], - "end_checklist": [ - "cargo fmt", - "Run the targeted tests you add or touch.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-test\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "completed", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test" + }, + { + "acceptance_criteria": [ + "Local integration gates are green for AHCSITC2 (fmt, clippy, tests, integ-checks).", + "Core integration branch is ready for checkpoint validation (`CP1-ci-checkpoint`)." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test", - "integration_task": "AHCSITC2-integ-core", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-test.md", + "concurrent_with": [], "depends_on": [ - "AHCSITC1-integ" + "AHCSITC2-code", + "AHCSITC2-test" ], - "concurrent_with": [ - "AHCSITC2-code" + "description": "Integrate AHCSITC2 implementation and make it green on the primary development platform before CP1.", + "end_checklist": [ + "cargo fmt", + "cargo clippy --workspace --all-targets -- -D warnings", + "Run relevant tests.", + "make integ-checks", + "On the orchestration branch, run checkpoint task: CP1-ci-checkpoint", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-core\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-test", - "required_make_targets": [ - "triad-test-checks" - ] - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core", "id": "AHCSITC2-integ-core", + "integration_task": "AHCSITC2-integ-core", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-core.md", + "merge_to_orchestration": false, "name": "AHCSITC2 slice (integration core)", - "type": "integration", "phase": "AHCSITC2", - "status": "pending", - "description": "Integrate AHCSITC2 implementation and make it green on the primary development platform before CP1.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -474,9 +494,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "Local integration gates are green for AHCSITC2 (fmt, clippy, tests, integ-checks).", - "Core integration branch is ready for checkpoint validation (`CP1-ci-checkpoint`)." + "required_make_targets": [ + "integ-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", @@ -484,36 +503,33 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-core\"" ], - "end_checklist": [ - "cargo fmt", - "cargo clippy --workspace --all-targets -- -D warnings", - "Run relevant tests.", - "make integ-checks", - "On the orchestration branch, run checkpoint task: CP1-ci-checkpoint", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-core\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core" + }, + { + "acceptance_criteria": [ + "The checkpoint audit trail is recorded in session_log.md.", + "Compile parity is green for the checkpoint checkout SHA.", + "Windows remains a compile-parity checkpoint lane only; no Windows feature-smoke run is required.", + "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core", - "integration_task": "AHCSITC2-integ-core", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-core.md", + "concurrent_with": [], "depends_on": [ - "AHCSITC2-code", - "AHCSITC2-test" + "AHCSITC2-integ-core" ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-core", - "required_make_targets": [ - "integ-checks" + "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC2.", + "end_checklist": [ + "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", + "Record any checkpoint blockers or required platform follow-ups in session_log.md.", + "Do not dispatch or require Windows feature smoke for this checkpoint.", + "Set status to completed; add END entry; commit docs." ], - "merge_to_orchestration": false - }, - { "id": "CP1-ci-checkpoint", + "integration_task": null, + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md", "name": "CP1 CI checkpoint (cross-platform parity)", - "type": "ops", "phase": "CP1", - "status": "pending", - "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC2.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -521,84 +537,39 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", "docs/project_management/system/standards/ci/PLANNING_CI_CHECKPOINT_STANDARD.md" ], - "acceptance_criteria": [ - "The checkpoint audit trail is recorded in session_log.md.", - "Compile parity is green for the checkpoint checkout SHA.", - "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." - ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs." ], - "end_checklist": [ - "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", - "Record any checkpoint blockers or required platform follow-ups in session_log.md.", - "Set status to completed; add END entry; commit docs." - ], - "worktree": null, - "integration_task": null, - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP1-ci-checkpoint.md", - "depends_on": [ - "AHCSITC2-integ-core" - ], - "concurrent_with": [] + "status": "completed", + "type": "ops", + "worktree": null }, { - "id": "AHCSITC2-integ-linux", - "name": "AHCSITC2 slice (integration Linux parity)", - "type": "integration", - "phase": "AHCSITC2", - "status": "pending", - "description": "Linux platform-fix integration task for AHCSITC2 parity failures.", - "references": [ - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/AHCSITC2-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" - ], "acceptance_criteria": [ "Linux compile and test parity is green for AHCSITC2." ], - "start_checklist": [ - "Run on a Linux host if possible.", - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-linux\"" + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint" ], + "description": "Linux platform-fix integration task for AHCSITC2 parity failures.", "end_checklist": [ "Run Linux-local parity gates (fmt, clippy, relevant tests) and capture outputs.", "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-linux\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux", + "id": "AHCSITC2-integ-linux", "integration_task": "AHCSITC2-integ-linux", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-linux.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint" - ], - "concurrent_with": [], - "platform": "linux", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux", - "required_make_targets": [ - "triad-code-checks" - ], - "merge_to_orchestration": false - }, - { - "id": "AHCSITC2-integ-macos", - "name": "AHCSITC2 slice (integration macOS parity)", - "type": "integration", + "merge_to_orchestration": false, + "name": "AHCSITC2 slice (integration Linux parity)", "phase": "AHCSITC2", - "status": "pending", - "description": "macOS platform-fix integration task for AHCSITC2 parity failures.", + "platform": "linux", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -609,44 +580,44 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "macOS compile and test parity is green for AHCSITC2." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ - "Run on a macOS host if possible.", + "Run on a Linux host if possible.", "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-macos\"" + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-linux\"" + ], + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-linux" + }, + { + "acceptance_criteria": [ + "macOS compile and test parity is green for AHCSITC2." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint" ], + "description": "macOS platform-fix integration task for AHCSITC2 parity failures.", "end_checklist": [ "Run macOS-local parity gates (fmt, clippy, relevant tests) and capture outputs.", "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-macos\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos", + "id": "AHCSITC2-integ-macos", "integration_task": "AHCSITC2-integ-macos", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-macos.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint" - ], - "concurrent_with": [], - "platform": "macos", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos", - "required_make_targets": [ - "triad-code-checks" - ], - "merge_to_orchestration": false - }, - { - "id": "AHCSITC2-integ-windows", - "name": "AHCSITC2 slice (integration Windows parity)", - "type": "integration", + "merge_to_orchestration": false, + "name": "AHCSITC2 slice (integration macOS parity)", "phase": "AHCSITC2", - "status": "pending", - "description": "Windows platform-fix integration task for AHCSITC2 parity failures.", + "platform": "macos", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -657,44 +628,44 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "Windows compile and test parity is green for AHCSITC2." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ - "Run on a Windows host if possible.", + "Run on a macOS host if possible.", "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-windows\"" + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-macos\"" + ], + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-macos" + }, + { + "acceptance_criteria": [ + "Windows compile and test parity is green for AHCSITC2." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint" ], + "description": "Windows platform-fix integration task for AHCSITC2 parity failures.", "end_checklist": [ "Run Windows-local parity gates (fmt, clippy, relevant tests) and capture outputs.", "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ-windows\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows", + "id": "AHCSITC2-integ-windows", "integration_task": "AHCSITC2-integ-windows", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ-windows.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint" - ], - "concurrent_with": [], - "platform": "windows", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows", - "required_make_targets": [ - "triad-code-checks" - ], - "merge_to_orchestration": false - }, - { - "id": "AHCSITC2-integ", - "name": "AHCSITC2 slice (integration final)", - "type": "integration", + "merge_to_orchestration": false, + "name": "AHCSITC2 slice (integration Windows parity)", "phase": "AHCSITC2", - "status": "pending", - "description": "Finalize AHCSITC2 across required CI parity platforms.", + "platform": "windows", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -703,14 +674,23 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "All required CI parity runs for AHCSITC2 are green.", - "Any platform-specific follow-ups are merged and reflected in session_log.md.", - "The final merged state implements the behaviors required by ac_ids." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "Run on a Windows host if possible.", + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ-windows\"" ], + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ-windows" + }, + { "ac_ids": [ "AC-AHCSITC2-01", "AC-AHCSITC2-02", @@ -719,12 +699,20 @@ "AC-AHCSITC2-05", "AC-AHCSITC2-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ\"" + "acceptance_criteria": [ + "All required CI parity runs for AHCSITC2 are green.", + "Any platform-specific follow-ups are merged and reflected in session_log.md.", + "The final merged state implements the behaviors required by ac_ids." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC2-integ-core", + "CP1-ci-checkpoint", + "AHCSITC2-integ-linux", + "AHCSITC2-integ-macos", + "AHCSITC2-integ-windows" ], + "description": "Finalize AHCSITC2 across required CI parity platforms.", "end_checklist": [ "Merge platform-fix branches (if any) and resolve conflicts.", "cargo fmt", @@ -734,43 +722,38 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC2-integ\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ", + "id": "AHCSITC2-integ", "integration_task": "AHCSITC2-integ", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/kickoff_prompts/AHCSITC2-integ.md", - "depends_on": [ - "AHCSITC2-integ-core", - "CP1-ci-checkpoint", - "AHCSITC2-integ-linux", - "AHCSITC2-integ-macos", - "AHCSITC2-integ-windows" - ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ", - "required_make_targets": [ - "integ-checks" - ], - "merge_to_orchestration": true - }, - { - "id": "AHCSITC3-code", - "name": "AHCSITC3 slice (code)", - "type": "code", - "phase": "AHCSITC3", - "status": "pending", - "description": "Implement the AHCSITC3 platform parity, compatibility, and validation behaviors.", + "merge_to_orchestration": true, + "name": "AHCSITC2 slice (integration final)", + "phase": "AHCSITC2", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/AHCSITC3-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC2/AHCSITC2-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/policy-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", - "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "Implements the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + "required_make_targets": [ + "integ-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC2-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC2-integ\"" ], + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc2-integ" + }, + { "ac_ids": [ "AC-AHCSITC3-01", "AC-AHCSITC3-02", @@ -779,40 +762,29 @@ "AC-AHCSITC3-05", "AC-AHCSITC3-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" + "acceptance_criteria": [ + "Implements the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + ], + "concurrent_with": [ + "AHCSITC3-test" + ], + "depends_on": [ + "CP1-ci-checkpoint", + "AHCSITC2-integ" ], + "description": "Implement the AHCSITC3 platform parity, compatibility, and validation behaviors.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-code\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code", + "id": "AHCSITC3-code", "integration_task": "AHCSITC3-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-code.md", - "depends_on": [ - "CP1-ci-checkpoint", - "AHCSITC2-integ" - ], - "concurrent_with": [ - "AHCSITC3-test" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code", - "required_make_targets": [ - "triad-code-checks" - ] - }, - { - "id": "AHCSITC3-test", - "name": "AHCSITC3 slice (test)", - "type": "test", + "name": "AHCSITC3 slice (code)", "phase": "AHCSITC3", - "status": "pending", - "description": "Add tests that enforce the AHCSITC3 platform parity, compatibility, and validation behaviors.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -823,9 +795,20 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + "required_make_targets": [ + "triad-code-checks" + ], + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" ], + "status": "completed", + "type": "code", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-code" + }, + { "ac_ids": [ "AC-AHCSITC3-01", "AC-AHCSITC3-02", @@ -834,40 +817,29 @@ "AC-AHCSITC3-05", "AC-AHCSITC3-06" ], - "start_checklist": [ - "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", - "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", - "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" + "acceptance_criteria": [ + "Tests enforce the behaviors required by ac_ids (see slices/AHCSITC3/AHCSITC3-spec.md)." + ], + "concurrent_with": [ + "AHCSITC3-code" + ], + "depends_on": [ + "CP1-ci-checkpoint", + "AHCSITC2-integ" ], + "description": "Add tests that enforce the AHCSITC3 platform parity, compatibility, and validation behaviors.", "end_checklist": [ "cargo fmt", "Run the targeted tests you add or touch.", "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-test\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test", + "id": "AHCSITC3-test", "integration_task": "AHCSITC3-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-test.md", - "depends_on": [ - "CP1-ci-checkpoint", - "AHCSITC2-integ" - ], - "concurrent_with": [ - "AHCSITC3-code" - ], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test", - "required_make_targets": [ - "triad-test-checks" - ] - }, - { - "id": "AHCSITC3-integ-core", - "name": "AHCSITC3 slice (integration core)", - "type": "integration", + "name": "AHCSITC3 slice (test)", "phase": "AHCSITC3", - "status": "pending", - "description": "Integrate AHCSITC3 implementation and make it green on the primary development platform before CP2.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -875,20 +847,33 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/AHCSITC3-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", - "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", - "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" + "docs/project_management/adrs/draft/ADR-0044-agent-hub-core-successor-identity-tuple-compatible.md" ], - "acceptance_criteria": [ - "Local integration gates are green for AHCSITC3 (fmt, clippy, tests, integ-checks).", - "Core integration branch is ready for checkpoint validation (`CP2-ci-checkpoint`)." + "required_make_targets": [ + "triad-test-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs.", - "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-core\"" + "Run: make triad-task-start-pair FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" SLICE_ID=\"AHCSITC3\"" + ], + "status": "completed", + "type": "test", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-test" + }, + { + "acceptance_criteria": [ + "Local integration gates are green for AHCSITC3 (fmt, clippy, tests, integ-checks).", + "Core integration branch is ready for checkpoint validation (`CP2-ci-checkpoint`)." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC3-code", + "AHCSITC3-test" ], + "description": "Integrate AHCSITC3 implementation and make it green on the primary development platform before CP2.", "end_checklist": [ "cargo fmt", "cargo clippy --workspace --all-targets -- -D warnings", @@ -898,27 +883,60 @@ "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-core\"", "Update tasks.json and session_log.md on the orchestration branch." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core", + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core", + "id": "AHCSITC3-integ-core", "integration_task": "AHCSITC3-integ-core", "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-core.md", - "depends_on": [ - "AHCSITC3-code", - "AHCSITC3-test" + "merge_to_orchestration": false, + "name": "AHCSITC3 slice (integration core)", + "phase": "AHCSITC3", + "references": [ + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/session_log.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/AHCSITC3-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/platform-parity-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/compatibility-spec.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", + "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", + "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core", "required_make_targets": [ "integ-checks" ], - "merge_to_orchestration": false + "start_checklist": [ + "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", + "Read plan.md, tasks.json, session_log.md, AHCSITC3-spec.md, and the kickoff prompt.", + "Set status to in_progress; add START entry; commit docs.", + "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-core\"" + ], + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-core" }, { + "acceptance_criteria": [ + "The checkpoint audit trail is recorded in session_log.md.", + "Compile parity is green for the checkpoint checkout SHA.", + "Windows remains a compile-parity checkpoint lane only; no Windows feature-smoke run is required.", + "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." + ], + "concurrent_with": [], + "depends_on": [ + "AHCSITC3-integ-core" + ], + "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC3.", + "end_checklist": [ + "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", + "Record any checkpoint blockers or required platform follow-ups in session_log.md.", + "Do not dispatch or require Windows feature smoke for this checkpoint.", + "Set status to completed; add END entry; commit docs." + ], "id": "CP2-ci-checkpoint", + "integration_task": null, + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md", "name": "CP2 CI checkpoint (cross-platform parity)", - "type": "ops", "phase": "CP2", - "status": "pending", - "description": "Run the cross-platform compile-parity gate for the checkpoint group ending at AHCSITC3.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -926,36 +944,39 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/impact_map.md", "docs/project_management/system/standards/ci/PLANNING_CI_CHECKPOINT_STANDARD.md" ], - "acceptance_criteria": [ - "The checkpoint audit trail is recorded in session_log.md.", - "Compile parity is green for the checkpoint checkout SHA.", - "The task only validates the checkpoint boundary defined by pre-planning/ci_checkpoint_plan.md." - ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Read pre-planning/ci_checkpoint_plan.md, tasks.json, session_log.md, and the kickoff prompt.", "Set status to in_progress; add START entry; commit docs." ], - "end_checklist": [ - "Dispatch compile parity for the checkpoint checkout SHA and record run ids/URLs.", - "Record any checkpoint blockers or required platform follow-ups in session_log.md.", - "Set status to completed; add END entry; commit docs." + "status": "completed", + "type": "ops", + "worktree": null + }, + { + "acceptance_criteria": [ + "Linux compile and test parity is green for AHCSITC3." ], - "worktree": null, - "integration_task": null, - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/CP2-ci-checkpoint.md", + "concurrent_with": [], "depends_on": [ - "AHCSITC3-integ-core" + "AHCSITC3-integ-core", + "CP2-ci-checkpoint" ], - "concurrent_with": [] - }, - { + "description": "Linux platform-fix integration task for AHCSITC3 parity failures.", + "end_checklist": [ + "Run Linux-local parity gates (fmt, clippy, relevant tests) and capture outputs.", + "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-linux\"", + "Update tasks.json and session_log.md on the orchestration branch." + ], + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux", "id": "AHCSITC3-integ-linux", + "integration_task": "AHCSITC3-integ-linux", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-linux.md", + "merge_to_orchestration": false, "name": "AHCSITC3 slice (integration Linux parity)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "Linux platform-fix integration task for AHCSITC3 parity failures.", + "platform": "linux", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -966,8 +987,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "Linux compile and test parity is green for AHCSITC3." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ "Run on a Linux host if possible.", @@ -976,34 +997,34 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-linux\"" ], - "end_checklist": [ - "Run Linux-local parity gates (fmt, clippy, relevant tests) and capture outputs.", - "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-linux\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux" + }, + { + "acceptance_criteria": [ + "macOS compile and test parity is green for AHCSITC3." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux", - "integration_task": "AHCSITC3-integ-linux", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-linux.md", + "concurrent_with": [], "depends_on": [ "AHCSITC3-integ-core", "CP2-ci-checkpoint" ], - "concurrent_with": [], - "platform": "linux", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-linux", - "required_make_targets": [ - "triad-code-checks" + "description": "macOS platform-fix integration task for AHCSITC3 parity failures.", + "end_checklist": [ + "Run macOS-local parity gates (fmt, clippy, relevant tests) and capture outputs.", + "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-macos\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "merge_to_orchestration": false - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos", "id": "AHCSITC3-integ-macos", + "integration_task": "AHCSITC3-integ-macos", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-macos.md", + "merge_to_orchestration": false, "name": "AHCSITC3 slice (integration macOS parity)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "macOS platform-fix integration task for AHCSITC3 parity failures.", + "platform": "macos", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1014,8 +1035,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "macOS compile and test parity is green for AHCSITC3." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ "Run on a macOS host if possible.", @@ -1024,34 +1045,34 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-macos\"" ], - "end_checklist": [ - "Run macOS-local parity gates (fmt, clippy, relevant tests) and capture outputs.", - "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-macos\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos" + }, + { + "acceptance_criteria": [ + "Windows compile and test parity is green for AHCSITC3." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos", - "integration_task": "AHCSITC3-integ-macos", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-macos.md", + "concurrent_with": [], "depends_on": [ "AHCSITC3-integ-core", "CP2-ci-checkpoint" ], - "concurrent_with": [], - "platform": "macos", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-macos", - "required_make_targets": [ - "triad-code-checks" + "description": "Windows platform-fix integration task for AHCSITC3 parity failures.", + "end_checklist": [ + "Run Windows-local parity gates (fmt, clippy, relevant tests) and capture outputs.", + "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-windows\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "merge_to_orchestration": false - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows", "id": "AHCSITC3-integ-windows", + "integration_task": "AHCSITC3-integ-windows", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-windows.md", + "merge_to_orchestration": false, "name": "AHCSITC3 slice (integration Windows parity)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "Windows platform-fix integration task for AHCSITC3 parity failures.", + "platform": "windows", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1062,8 +1083,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md" ], - "acceptance_criteria": [ - "Windows compile and test parity is green for AHCSITC3." + "required_make_targets": [ + "triad-code-checks" ], "start_checklist": [ "Run on a Windows host if possible.", @@ -1072,34 +1093,49 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ-windows\"" ], - "end_checklist": [ - "Run Windows-local parity gates (fmt, clippy, relevant tests) and capture outputs.", - "If parity still fails remotely, record the compile-parity run id/URL that justified this task.", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ-windows\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows" + }, + { + "ac_ids": [ + "AC-AHCSITC3-01", + "AC-AHCSITC3-02", + "AC-AHCSITC3-03", + "AC-AHCSITC3-04", + "AC-AHCSITC3-05", + "AC-AHCSITC3-06" ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows", - "integration_task": "AHCSITC3-integ-windows", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ-windows.md", + "acceptance_criteria": [ + "All required CI parity runs for AHCSITC3 are green.", + "Any platform-specific follow-ups are merged and reflected in session_log.md.", + "The final merged state implements the behaviors required by ac_ids." + ], + "concurrent_with": [], "depends_on": [ "AHCSITC3-integ-core", - "CP2-ci-checkpoint" + "CP2-ci-checkpoint", + "AHCSITC3-integ-linux", + "AHCSITC3-integ-macos", + "AHCSITC3-integ-windows" ], - "concurrent_with": [], - "platform": "windows", - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ-windows", - "required_make_targets": [ - "triad-code-checks" + "description": "Finalize AHCSITC3 across required CI parity platforms.", + "end_checklist": [ + "Merge platform-fix branches (if any) and resolve conflicts.", + "cargo fmt", + "cargo clippy --workspace --all-targets -- -D warnings", + "Run relevant tests.", + "make integ-checks", + "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ\"", + "Update tasks.json and session_log.md on the orchestration branch." ], - "merge_to_orchestration": false - }, - { + "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ", "id": "AHCSITC3-integ", + "integration_task": "AHCSITC3-integ", + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ.md", + "merge_to_orchestration": true, "name": "AHCSITC3 slice (integration final)", - "type": "integration", "phase": "AHCSITC3", - "status": "pending", - "description": "Finalize AHCSITC3 across required CI parity platforms.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1111,18 +1147,8 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/manual_testing_playbook.md", "docs/project_management/system/standards/triad/TASK_TRIADS_WORKTREE_EXECUTION_STANDARD.md" ], - "acceptance_criteria": [ - "All required CI parity runs for AHCSITC3 are green.", - "Any platform-specific follow-ups are merged and reflected in session_log.md.", - "The final merged state implements the behaviors required by ac_ids." - ], - "ac_ids": [ - "AC-AHCSITC3-01", - "AC-AHCSITC3-02", - "AC-AHCSITC3-03", - "AC-AHCSITC3-04", - "AC-AHCSITC3-05", - "AC-AHCSITC3-06" + "required_make_targets": [ + "integ-checks" ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", @@ -1130,39 +1156,31 @@ "Set status to in_progress; add START entry; commit docs.", "Run: make triad-task-start FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" TASK_ID=\"AHCSITC3-integ\"" ], - "end_checklist": [ - "Merge platform-fix branches (if any) and resolve conflicts.", - "cargo fmt", - "cargo clippy --workspace --all-targets -- -D warnings", - "Run relevant tests.", - "make integ-checks", - "From inside the worktree: make triad-task-finish TASK_ID=\"AHCSITC3-integ\"", - "Update tasks.json and session_log.md on the orchestration branch." + "status": "completed", + "type": "integration", + "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ" + }, + { + "acceptance_criteria": [ + "Feature directory docs remain consistent after cleanup.", + "All temporary worktrees are removed.", + "The cleanup summary is recorded in session_log.md." ], - "worktree": "wt/agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ", - "integration_task": "AHCSITC3-integ", - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/slices/AHCSITC3/kickoff_prompts/AHCSITC3-integ.md", + "concurrent_with": [], "depends_on": [ - "AHCSITC3-integ-core", - "CP2-ci-checkpoint", - "AHCSITC3-integ-linux", - "AHCSITC3-integ-macos", - "AHCSITC3-integ-windows" + "AHCSITC3-integ" ], - "concurrent_with": [], - "git_branch": "agent-hub-core-successor-identity-tuple-compatible-ahcsitc3-integ", - "required_make_targets": [ - "integ-checks" + "description": "Close out the feature task graph and remove retained worktrees.", + "end_checklist": [ + "Run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" DRY_RUN=1 REMOVE_WORKTREES=1 PRUNE_LOCAL=1", + "Then run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" REMOVE_WORKTREES=1 PRUNE_LOCAL=1", + "Set status to completed; add END entry with the cleanup summary; commit docs." ], - "merge_to_orchestration": true - }, - { "id": "FZ-feature-cleanup", + "integration_task": null, + "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/FZ-feature-cleanup.md", "name": "Feature cleanup (worktrees + branches)", - "type": "ops", "phase": "FZ", - "status": "pending", - "description": "Close out the feature task graph and remove retained worktrees.", "references": [ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/plan.md", "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/tasks.json", @@ -1171,28 +1189,14 @@ "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/pre-planning/ci_checkpoint_plan.md", "docs/project_management/system/standards/triad/TASK_TRIADS_AND_FEATURE_SETUP.md" ], - "acceptance_criteria": [ - "Feature directory docs remain consistent after cleanup.", - "All temporary worktrees are removed.", - "The cleanup summary is recorded in session_log.md." - ], "start_checklist": [ "git checkout feat/agent-hub-core-successor-identity-tuple-compatible && git pull --ff-only", "Confirm all tasks are completed and merged as intended.", "Set status to in_progress; add START entry; commit docs." ], - "end_checklist": [ - "Run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" DRY_RUN=1 REMOVE_WORKTREES=1 PRUNE_LOCAL=1", - "Then run: make triad-feature-cleanup FEATURE_DIR=\"docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible\" REMOVE_WORKTREES=1 PRUNE_LOCAL=1", - "Set status to completed; add END entry with the cleanup summary; commit docs." - ], - "worktree": null, - "integration_task": null, - "kickoff_prompt": "docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/kickoff_prompts/FZ-feature-cleanup.md", - "depends_on": [ - "AHCSITC3-integ" - ], - "concurrent_with": [] + "status": "pending", + "type": "ops", + "worktree": null } ] } diff --git a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md index 7cffc184e..84555d947 100644 --- a/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md +++ b/docs/project_management/packs/draft/agent-hub-core-successor-identity-tuple-compatible/telemetry-spec.md @@ -122,6 +122,7 @@ Omission rules: - `client` on the nested record stays equal to the parent agent runtime id. - `router`, `provider`, `auth_authority`, and `protocol` on the nested record describe only the nested gateway-backed request. - The nested record never mutates, widens, or retroactively annotates the parent pure-agent record. +- Status consumers use `parent_run_id` for correlation validation only. A nested row is emitted only when its `parent_run_id` matches the winning selected pure-agent `run_id` for that `(orchestration_session_id, agent_id)` pair. Nested rows tied to older historical pure-agent runs are ignored as stale history, and malformed selected-surface rows fail closed. ## `world_generation` publication path