@team-agent/installer 0.5.119 → 0.5.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +46 -1
- package/README.zh.md +46 -1
- package/crates/team-agent/src/cli/adapters.rs +11 -0
- package/crates/team-agent/src/cli/arguments.rs +401 -0
- package/crates/team-agent/src/cli/emit.rs +241 -142
- package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
- package/crates/team-agent/src/cli/intent/tests.rs +654 -0
- package/crates/team-agent/src/cli/intent.rs +642 -0
- package/crates/team-agent/src/cli/mod.rs +53 -6
- package/crates/team-agent/src/cli/named_address.rs +10 -1
- package/crates/team-agent/src/cli/spec.rs +22 -10
- package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
- package/crates/team-agent/src/cli/tests/base.rs +6 -1
- package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +65 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
- package/crates/team-agent/src/lifecycle/launch.rs +29 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
- package/crates/team-agent/src/lifecycle/restart.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
- package/crates/team-agent/src/lifecycle/types.rs +10 -0
- package/crates/team-agent/src/mcp_server/mod.rs +2 -0
- package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
- package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
- package/crates/team-agent/src/mcp_server/wire.rs +96 -11
- package/crates/team-agent/src/messaging/delivery.rs +62 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
- package/crates/team-agent/src/provider/adapter.rs +7 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
- package/crates/team-agent/src/tmux_backend.rs +8 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +71 -54
- package/skills/team-agent/command-coverage.json +283 -13
- package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
- package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +61 -212
- package/skills/team-agent/references/team-agent-operator.md +181 -48
- package/skills/team-agent/references/team-in-team.md +47 -80
|
@@ -57,6 +57,8 @@ pub(crate) const STATUS_REMINDER: &str =
|
|
|
57
57
|
"Next: use team-agent inbox leader -n 3 for replies. Status is not proof of task completion; do not inspect worker terminal content.";
|
|
58
58
|
|
|
59
59
|
pub mod adapters;
|
|
60
|
+
mod arguments;
|
|
61
|
+
mod intent;
|
|
60
62
|
#[cfg(unix)]
|
|
61
63
|
mod shutdown_socket;
|
|
62
64
|
pub mod attach_app_server_leader;
|
|
@@ -194,7 +196,7 @@ pub mod lifecycle_port {
|
|
|
194
196
|
crate::leader::LeaderLaunchStatus::NotStarted => false,
|
|
195
197
|
};
|
|
196
198
|
let leader_attach_command = if outcome.reason.as_deref() == Some("PaneWorkspaceMismatch") {
|
|
197
|
-
Some("team-agent attach-leader"
|
|
199
|
+
Some(format!("team-agent attach-leader --provider {}", crate::provider::wire::provider_wire(provider)))
|
|
198
200
|
} else {
|
|
199
201
|
leader_attach_command_for_plan(cwd, &plan)
|
|
200
202
|
};
|
|
@@ -387,14 +389,16 @@ pub mod lifecycle_port {
|
|
|
387
389
|
}
|
|
388
390
|
|
|
389
391
|
let hint_action = match refusal.recovery.hint_action {
|
|
390
|
-
PaneAuthorityRecoveryHint::AttachLeader =>
|
|
392
|
+
PaneAuthorityRecoveryHint::AttachLeader => format!(
|
|
393
|
+
"team-agent attach-leader --provider {}", crate::provider::wire::provider_wire(provider)
|
|
394
|
+
),
|
|
391
395
|
};
|
|
392
396
|
let action = match refusal.recovery.action {
|
|
393
397
|
PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane => {
|
|
394
|
-
"open a new terminal window outside the current tmux/pane, confirm TMUX and \
|
|
398
|
+
format!("open a new terminal window outside the current tmux/pane, confirm TMUX and \
|
|
395
399
|
TMUX_PANE are absent, change directory to the requested workspace, and restart \
|
|
396
400
|
the leader; alternatively enter the matching workspace pane and run \
|
|
397
|
-
`
|
|
401
|
+
`{hint_action}`")
|
|
398
402
|
}
|
|
399
403
|
};
|
|
400
404
|
facts.insert(
|
|
@@ -1720,7 +1724,10 @@ pub mod lifecycle_port {
|
|
|
1720
1724
|
let wp = crate::coordinator::WorkspacePath::new(run_workspace.clone());
|
|
1721
1725
|
let coordinator_pid_before_stop = crate::coordinator::coordinator_health(&wp).pid;
|
|
1722
1726
|
coordinator_pid_for_report = coordinator_pid_before_stop.map(|pid| pid.get());
|
|
1723
|
-
|
|
1727
|
+
// Owned termination can use 5s graceful + 5s escalation waits.
|
|
1728
|
+
// Await its result within the existing shutdown deadline, not an
|
|
1729
|
+
// unrelated 900ms cap that abandons a still-finishing stop worker.
|
|
1730
|
+
match stop_coordinator_bounded(wp, deadline.remaining()) {
|
|
1724
1731
|
Some(Ok(report)) => Some(report),
|
|
1725
1732
|
Some(Err(error)) => {
|
|
1726
1733
|
kill_error.get_or_insert(error);
|
|
@@ -4091,6 +4098,25 @@ pub mod lifecycle_port {
|
|
|
4091
4098
|
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
4092
4099
|
"readiness": readiness_json.clone(),
|
|
4093
4100
|
"worker_readiness": readiness_json,
|
|
4101
|
+
"effects": {"worker_panes_started": agent_ids, "coordinator_start": "running_confirmed"},
|
|
4102
|
+
})
|
|
4103
|
+
}
|
|
4104
|
+
crate::lifecycle::QuickStartReport::Partial {
|
|
4105
|
+
workspace, session_name, launch, team, error, coordinator_state,
|
|
4106
|
+
} => {
|
|
4107
|
+
let agents: Vec<_> = launch.started.iter().map(|agent| agent.agent_id.as_str())
|
|
4108
|
+
.chain(launch.contract_started.iter().map(|id| id.as_str())).collect();
|
|
4109
|
+
let recovery = format!(
|
|
4110
|
+
"team-agent doctor --workspace {} --team {} --json; inspect confirmed effects before any retry; no automatic rollback is claimed",
|
|
4111
|
+
shell_quote(&workspace.to_string_lossy()), shell_quote(&team),
|
|
4112
|
+
);
|
|
4113
|
+
json!({
|
|
4114
|
+
"ok": false, "status": "partial", "phase": "post_spawn",
|
|
4115
|
+
"workspace": workspace, "team": team, "session_name": session_name.as_str(),
|
|
4116
|
+
"error": crate::redaction::redact_external_text(&error),
|
|
4117
|
+
"effects": {"worker_panes_started": agents, "coordinator_start": coordinator_state},
|
|
4118
|
+
"last_confirmed_effect": if agents.is_empty() {Value::Null} else {json!("worker_panes_started")},
|
|
4119
|
+
"current_state": "unknown", "recovery": recovery,
|
|
4094
4120
|
})
|
|
4095
4121
|
}
|
|
4096
4122
|
crate::lifecycle::QuickStartReport::ExistingRuntime {
|
|
@@ -4122,6 +4148,7 @@ pub mod lifecycle_port {
|
|
|
4122
4148
|
"ok": false,
|
|
4123
4149
|
"status": "preflight_blocked",
|
|
4124
4150
|
"reason": "quick-start preflight blocked",
|
|
4151
|
+
"effects": {"worker_panes_started": [], "coordinator_start": "not_attempted"},
|
|
4125
4152
|
"summary": summary,
|
|
4126
4153
|
"blockers": blockers,
|
|
4127
4154
|
"next_actions": next_actions,
|
|
@@ -4205,6 +4232,25 @@ pub mod lifecycle_port {
|
|
|
4205
4232
|
.is_some());
|
|
4206
4233
|
}
|
|
4207
4234
|
|
|
4235
|
+
#[test]
|
|
4236
|
+
fn issue337_partial_start_receipt_retains_workers_and_scoped_recovery() {
|
|
4237
|
+
let crate::lifecycle::QuickStartReport::Ready { launch, .. } = pending_tool_load_report() else { panic!() };
|
|
4238
|
+
let value = quick_start_value(crate::lifecycle::QuickStartReport::Partial {
|
|
4239
|
+
workspace: PathBuf::from("/owned/work space"),
|
|
4240
|
+
session_name: crate::transport::SessionName::new("team-demo"),
|
|
4241
|
+
launch, team: "demo".into(), error: "caller disappeared".into(),
|
|
4242
|
+
coordinator_state: "not_attempted".into(),
|
|
4243
|
+
});
|
|
4244
|
+
assert_eq!(value["status"], "partial");
|
|
4245
|
+
assert_eq!(value["effects"]["worker_panes_started"], json!(["worker"]));
|
|
4246
|
+
assert_eq!(value["effects"]["coordinator_start"], "not_attempted");
|
|
4247
|
+
assert_eq!(value["last_confirmed_effect"], "worker_panes_started");
|
|
4248
|
+
let recovery = value["recovery"].as_str().unwrap();
|
|
4249
|
+
let argv = split_shell_argv(recovery);
|
|
4250
|
+
assert!(argv.windows(2).any(|pair| pair == ["--workspace", "/owned/work space"]));
|
|
4251
|
+
assert!(argv.windows(2).any(|pair| pair == ["--team", "demo"]));
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4208
4254
|
fn pending_tool_load_report() -> crate::lifecycle::QuickStartReport {
|
|
4209
4255
|
crate::lifecycle::QuickStartReport::Ready {
|
|
4210
4256
|
session_name: crate::transport::SessionName::new("team-demo"),
|
|
@@ -4425,7 +4471,7 @@ pub mod lifecycle_port {
|
|
|
4425
4471
|
#[test]
|
|
4426
4472
|
#[serial_test::serial(env)]
|
|
4427
4473
|
fn existing_runtime_producer_scopes_send_team() {
|
|
4428
|
-
let _hermetic = super::super::hermetic_test_support::HermeticTestEnv::
|
|
4474
|
+
let _hermetic = super::super::hermetic_test_support::HermeticTestEnv::enter_with_leader(
|
|
4429
4475
|
"quick-start-value-existing-runtime",
|
|
4430
4476
|
);
|
|
4431
4477
|
let root = std::env::temp_dir().join(format!(
|
|
@@ -4448,6 +4494,7 @@ pub mod lifecycle_port {
|
|
|
4448
4494
|
"---\nname: implementer\nrole: Implementation Engineer\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nImplement bounded tasks.\n",
|
|
4449
4495
|
)
|
|
4450
4496
|
.unwrap();
|
|
4497
|
+
super::super::hermetic_test_support::seed_in_process_coordinator(&root);
|
|
4451
4498
|
let transport = crate::transport::test_support::OfflineTransport::new();
|
|
4452
4499
|
let first = crate::lifecycle::quick_start_with_transport(
|
|
4453
4500
|
&team_dir,
|
|
@@ -1050,6 +1050,7 @@ fn resolve_leader(
|
|
|
1050
1050
|
None,
|
|
1051
1051
|
None,
|
|
1052
1052
|
transport.tmux_endpoint(),
|
|
1053
|
+
None,
|
|
1053
1054
|
);
|
|
1054
1055
|
err.candidates =
|
|
1055
1056
|
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
@@ -1111,6 +1112,7 @@ fn resolve_leader(
|
|
|
1111
1112
|
session,
|
|
1112
1113
|
window,
|
|
1113
1114
|
socket,
|
|
1115
|
+
string_field(receiver, "provider"),
|
|
1114
1116
|
);
|
|
1115
1117
|
if matches!(
|
|
1116
1118
|
&reason,
|
|
@@ -1131,6 +1133,7 @@ fn resolve_leader(
|
|
|
1131
1133
|
session,
|
|
1132
1134
|
window,
|
|
1133
1135
|
socket,
|
|
1136
|
+
string_field(receiver, "provider"),
|
|
1134
1137
|
);
|
|
1135
1138
|
err.log = format!("{} channel_probe_failed={error}", err.log);
|
|
1136
1139
|
Err(err)
|
|
@@ -1574,14 +1577,20 @@ fn leader_not_attached_third_party(
|
|
|
1574
1577
|
session: Option<&str>,
|
|
1575
1578
|
window: Option<&str>,
|
|
1576
1579
|
socket: Option<String>,
|
|
1580
|
+
provider: Option<&str>,
|
|
1577
1581
|
) -> NamedAddressError {
|
|
1578
1582
|
let workspace = target_workspace.display();
|
|
1579
1583
|
let mut err = NamedAddressError::new(
|
|
1580
1584
|
NamedAddressErrorKind::LeaderNotAttached,
|
|
1581
1585
|
"target team leader is not attached on its recorded tmux endpoint",
|
|
1582
1586
|
);
|
|
1587
|
+
let provider = provider.and_then(crate::provider::wire::parse_provider)
|
|
1588
|
+
.map(crate::provider::wire::provider_wire);
|
|
1589
|
+
let workspace_arg = super::adapters::shell_quote(&target_workspace.to_string_lossy());
|
|
1590
|
+
let team_arg = super::adapters::shell_quote(team);
|
|
1583
1591
|
err.action = format!(
|
|
1584
|
-
"Ask the target team owner to run `team-agent attach-leader --workspace {
|
|
1592
|
+
"Ask the target team owner to select the actual Leader provider and run `team-agent attach-leader --workspace {workspace_arg} --team {team_arg} --provider {}`; the sender-side offline mailbox will requeue the message once attach succeeds",
|
|
1593
|
+
provider.unwrap_or("<provider>")
|
|
1585
1594
|
);
|
|
1586
1595
|
err.log = format!(
|
|
1587
1596
|
"name={team}/leader expected_pane={} expected_session={} expected_window={} tmux_socket={}",
|
|
@@ -32,6 +32,10 @@ pub(crate) enum TokenUsage {
|
|
|
32
32
|
}
|
|
33
33
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
34
34
|
pub(crate) enum DispatchKind {
|
|
35
|
+
Start,
|
|
36
|
+
Stop,
|
|
37
|
+
Team,
|
|
38
|
+
Agent,
|
|
35
39
|
QuickStart,
|
|
36
40
|
Send,
|
|
37
41
|
Status,
|
|
@@ -59,6 +63,10 @@ pub(crate) enum DispatchKind {
|
|
|
59
63
|
AttachLeader,
|
|
60
64
|
}
|
|
61
65
|
pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
|
|
66
|
+
DispatchKind::Start,
|
|
67
|
+
DispatchKind::Stop,
|
|
68
|
+
DispatchKind::Team,
|
|
69
|
+
DispatchKind::Agent,
|
|
62
70
|
DispatchKind::QuickStart,
|
|
63
71
|
DispatchKind::Send,
|
|
64
72
|
DispatchKind::Status,
|
|
@@ -110,20 +118,24 @@ pub(crate) struct CommandSpec {
|
|
|
110
118
|
// Only human operations belong here. Compatibility commands are private in emit.rs.
|
|
111
119
|
#[rustfmt::skip]
|
|
112
120
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
113
|
-
CommandSpec { name: "
|
|
121
|
+
CommandSpec { name: "start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Start), summary: "Start a team or agent; verified live instances stay unchanged", usage: "team-agent start [AGENT|./TEAMDIR] [--from TEAMDIR] [--workspace PATH] [--team KEY] [--session preserve|auto] [--dry-run] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
122
|
+
CommandSpec { name: "stop", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Stop), summary: "Stop the selected team or one agent, retaining session associations", usage: "team-agent stop [AGENT] [--workspace PATH] [--team KEY] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
123
|
+
CommandSpec { name: "team", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Team), summary: "Explicit team start, stop, restart or status", usage: "team-agent team <start|stop|restart|status> [OPTIONS]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
124
|
+
CommandSpec { name: "agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::Agent), summary: "Explicit agent lifecycle operations", usage: "team-agent agent <start|stop|restart|add|remove|reset|clone|fork> AGENT [OPTIONS]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
125
|
+
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "Compatibility creation bridge; prefer start for daily activation", usage: "team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--team TEAM] [--name NAME] [--team-id TEAM] [--yes] [--backend tmux|conpty] [--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
114
126
|
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "Send a task to a specified agent", usage: "team-agent send <agent> MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: Some("a future compatibility release"), action: Some("Use team-agent send <agent> 'task message'; select scope with --workspace and --team"), governance: None },
|
|
115
127
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "Check agent status without reading conversation content", usage: "team-agent status [<agent>] [--workspace WORKSPACE] [--team TEAM] [--json] [--summary|--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
116
128
|
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "Discover model names supported by the native provider", usage: "team-agent models [--provider pi|cursor|cursor_agent|codex|claude|claude_code|grok|kiro] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
117
129
|
CommandSpec { name: "inbox", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "Read agent replies", usage: "team-agent inbox <agent> [-n N|--limit N] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
118
|
-
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "Restart
|
|
119
|
-
CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "
|
|
120
|
-
CommandSpec { name: "add-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::AddAgent), summary: "
|
|
121
|
-
CommandSpec { name: "start-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StartAgent), summary: "
|
|
122
|
-
CommandSpec { name: "stop-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StopAgent), summary: "
|
|
123
|
-
CommandSpec { name: "reset-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ResetAgent), summary: "
|
|
124
|
-
CommandSpec { name: "clone-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "
|
|
125
|
-
CommandSpec { name: "fork-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "
|
|
126
|
-
CommandSpec { name: "remove-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "
|
|
130
|
+
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "Restart one agent, or use the frozen bare/path team bridge", usage: "team-agent restart [AGENT|./WORKSPACE] [--workspace WORKSPACE] [--team TEAM] [--session auto|preserve] [--dry-run] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
131
|
+
CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "Compatibility: shut down the selected team; prefer stop", usage: "team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
132
|
+
CommandSpec { name: "add-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::AddAgent), summary: "Compatibility: strict add-and-start; prefer agent add or start AGENT", usage: "team-agent add-agent <agent> [--role-file FILE] [--provider TOOL] [--model MODEL] [--effort LEVEL] [--bypass true|false] [--prompt TEXT] [--profile NAME] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
133
|
+
CommandSpec { name: "start-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StartAgent), summary: "Compatibility: start an existing stopped agent; prefer start AGENT", usage: "team-agent start-agent <agent> [--provider TOOL] [--model MODEL] [--effort LEVEL] [--bypass true|false] [--prompt TEXT] [--profile NAME] [--workspace WORKSPACE] [--team TEAM] [--allow-fresh] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
134
|
+
CommandSpec { name: "stop-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StopAgent), summary: "Compatibility: stop an agent; prefer stop AGENT", usage: "team-agent stop-agent <agent> [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
135
|
+
CommandSpec { name: "reset-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ResetAgent), summary: "Compatibility: discard session only with approval; prefer agent reset", usage: "team-agent reset-agent <agent> --discard-session [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
136
|
+
CommandSpec { name: "clone-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "Compatibility: copy agent configuration; prefer agent clone", usage: "team-agent clone-agent <agent> --as NEW_AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
137
|
+
CommandSpec { name: "fork-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "Compatibility: admitted new-seat continuation; prefer agent fork", usage: "team-agent fork-agent <agent> --as NEW_AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
138
|
+
CommandSpec { name: "remove-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "Compatibility: confirmed removal; prefer agent remove", usage: "team-agent remove-agent <agent> --confirm [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--force] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
127
139
|
CommandSpec { name: "leaders", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Leaders), summary: "List local leaders and their team scopes", usage: "team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
128
140
|
CommandSpec { name: "doctor", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Doctor), summary: "Diagnose a team and suggest recovery; read-only by default", usage: "team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--comms] [--gate orphans|comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
129
141
|
CommandSpec { name: "approvals", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "List pending agent approvals", usage: "team-agent approvals [<agent>] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
@@ -115,6 +115,7 @@ struct BriefNode {
|
|
|
115
115
|
health: String,
|
|
116
116
|
session_name: Option<String>,
|
|
117
117
|
tmux_command: Option<String>,
|
|
118
|
+
observation_reason: Option<&'static str>,
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
@@ -125,9 +126,10 @@ struct AttachContext {
|
|
|
125
126
|
pane: String,
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
#[derive(Debug)]
|
|
129
|
+
#[derive(Debug, Default)]
|
|
129
130
|
struct ProbeResult {
|
|
130
131
|
nodes: Vec<ProbeNode>,
|
|
132
|
+
exited: BTreeSet<(String, String, String)>,
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
#[derive(Debug, Deserialize)]
|
|
@@ -158,7 +160,7 @@ pub(crate) fn status_brief_scoped(workspace: &Path, state: &Value, agent: Option
|
|
|
158
160
|
json!({
|
|
159
161
|
"nodes": nodes
|
|
160
162
|
.into_iter()
|
|
161
|
-
.map(|(node, _)| node
|
|
163
|
+
.map(|(node, _)| scoped_node_json(node, workspace, state))
|
|
162
164
|
.collect::<Vec<_>>()
|
|
163
165
|
})
|
|
164
166
|
}
|
|
@@ -167,14 +169,22 @@ pub(crate) fn status_brief_scoped(workspace: &Path, state: &Value, agent: Option
|
|
|
167
169
|
/// context only when multiple nodes make that repetition useful.
|
|
168
170
|
pub(crate) fn format_status_brief(workspace: &Path, state: &Value, agent: Option<&str>) -> String {
|
|
169
171
|
let nodes = project_status_nodes(workspace, state, agent);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
let diagnostics = nodes.iter().filter(|(node, _)| node.observation_reason.is_some())
|
|
173
|
+
.map(|(node, _)| {
|
|
174
|
+
let value = scoped_node_json(node.clone(), workspace, state);
|
|
175
|
+
format!("observation {}: {}; next_action: {}", node.name,
|
|
176
|
+
value["observation_reason"].as_str().unwrap_or("observation_inconclusive"),
|
|
177
|
+
value["next_action"].as_str().unwrap_or(""))
|
|
178
|
+
}).collect::<Vec<_>>();
|
|
179
|
+
let mut output = if nodes.len() <= 1 {
|
|
180
|
+
nodes.into_iter().filter_map(|(node, _)| format_brief_node(&node.into_json()))
|
|
181
|
+
.collect::<Vec<_>>().join("\n")
|
|
182
|
+
} else { format_multi_node_brief(nodes) };
|
|
183
|
+
if !diagnostics.is_empty() {
|
|
184
|
+
output.push('\n');
|
|
185
|
+
output.push_str(&diagnostics.join("\n"));
|
|
176
186
|
}
|
|
177
|
-
|
|
187
|
+
output
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
fn project_status_nodes(
|
|
@@ -200,13 +210,18 @@ fn project_status_nodes(
|
|
|
200
210
|
.map(|node| {
|
|
201
211
|
#[cfg(unix)]
|
|
202
212
|
if crate::contract_runtime::registry::descriptor(&node.provider).is_some() {
|
|
203
|
-
let mut brief = BriefNode::unknown(node);
|
|
213
|
+
let mut brief = BriefNode::unknown(node).with_reason("native_runtime_projection_unavailable");
|
|
204
214
|
if let Ok(native) = crate::contract_runtime::framework::project_seat(
|
|
205
215
|
_workspace,
|
|
206
216
|
&crate::state::projection::team_state_key(state),
|
|
207
217
|
&node.name,
|
|
208
218
|
) {
|
|
209
219
|
brief.runtime_status = native["status"].as_str().unwrap_or("unknown").into();
|
|
220
|
+
brief.observation_reason = match brief.runtime_status.as_str() {
|
|
221
|
+
"running" => Some("activity_or_health_not_observed"),
|
|
222
|
+
"stopped" => None,
|
|
223
|
+
_ => Some("native_runtime_not_ready"),
|
|
224
|
+
};
|
|
210
225
|
brief.model = native["model"].as_str().map(str::to_owned);
|
|
211
226
|
brief.effort = native["effort"].as_str().map(str::to_owned);
|
|
212
227
|
if brief.runtime_status == "running" {
|
|
@@ -596,11 +611,14 @@ fn sample_native_panes(
|
|
|
596
611
|
} else {
|
|
597
612
|
snapshots(&pids)
|
|
598
613
|
};
|
|
614
|
+
let exited = panes.iter().filter(|fields| {
|
|
615
|
+
native_pane_pid(fields[3]).and_then(|pid| processes.get(&pid)).is_some_and(|p| p.zombie)
|
|
616
|
+
}).map(|fields| (fields[0].to_string(), fields[1].to_string(), fields[2].to_string())).collect();
|
|
599
617
|
let nodes = panes
|
|
600
618
|
.into_iter()
|
|
601
619
|
.filter_map(|fields| parse_native_pane(fields, endpoint, registered, &processes))
|
|
602
620
|
.collect();
|
|
603
|
-
ProbeResult { nodes }
|
|
621
|
+
ProbeResult { nodes, exited }
|
|
604
622
|
}
|
|
605
623
|
|
|
606
624
|
fn native_pane_fields(line: &str) -> Option<[&str; 5]> {
|
|
@@ -1199,7 +1217,7 @@ fn parse_probe(bytes: &[u8], requested_endpoint: &str) -> Result<ProbeResult, ()
|
|
|
1199
1217
|
.iter()
|
|
1200
1218
|
.filter_map(|node| parse_probe_node(node, report_socket))
|
|
1201
1219
|
.collect();
|
|
1202
|
-
Ok(ProbeResult { nodes })
|
|
1220
|
+
Ok(ProbeResult { nodes, ..Default::default() })
|
|
1203
1221
|
}
|
|
1204
1222
|
|
|
1205
1223
|
#[cfg(test)]
|
|
@@ -1273,18 +1291,34 @@ fn project_node_with_attach(
|
|
|
1273
1291
|
health: "unknown".to_string(),
|
|
1274
1292
|
session_name: None,
|
|
1275
1293
|
tmux_command: None,
|
|
1294
|
+
observation_reason: None,
|
|
1276
1295
|
},
|
|
1277
|
-
MatchResult::Unique(_) | MatchResult::Ambiguous => BriefNode::unknown(node),
|
|
1296
|
+
MatchResult::Unique(_) | MatchResult::Ambiguous => BriefNode::unknown(node).with_reason("stopped_registration_has_live_pane"),
|
|
1278
1297
|
};
|
|
1279
1298
|
return (projected, None);
|
|
1280
1299
|
}
|
|
1281
1300
|
let MatchResult::Unique(observed) = matched else {
|
|
1282
|
-
|
|
1301
|
+
let reason = if matches!(matched, MatchResult::Ambiguous) {
|
|
1302
|
+
"pane_observation_ambiguous"
|
|
1303
|
+
} else if node.endpoint.is_none() {
|
|
1304
|
+
"endpoint_unregistered"
|
|
1305
|
+
} else if node.session.is_none() || node.pane.is_none() {
|
|
1306
|
+
"pane_binding_incomplete"
|
|
1307
|
+
} else if let Some(sample) = probe.and_then(Option::as_ref) {
|
|
1308
|
+
if sample.exited.iter().any(|(session, window, pane)| {
|
|
1309
|
+
node.session.as_deref() == Some(session.as_str())
|
|
1310
|
+
&& node.window.as_deref().is_none_or(|name| name == window)
|
|
1311
|
+
&& node.pane.as_deref() == Some(pane.as_str())
|
|
1312
|
+
}) { "pane_process_exited" }
|
|
1313
|
+
else if matches!(node.lifecycle.as_deref(), Some("starting" | "launching" | "pending")) { "startup_not_yet_observed" }
|
|
1314
|
+
else { "registered_pane_not_observed" }
|
|
1315
|
+
} else { "endpoint_probe_unavailable" };
|
|
1316
|
+
return (BriefNode::unknown(node).with_reason(reason), None);
|
|
1283
1317
|
};
|
|
1284
1318
|
if !node.provider.eq_ignore_ascii_case("unknown")
|
|
1285
1319
|
&& !node.provider.eq_ignore_ascii_case(&observed.provider)
|
|
1286
1320
|
{
|
|
1287
|
-
return (BriefNode::unknown(node), None);
|
|
1321
|
+
return (BriefNode::unknown(node).with_reason("provider_observation_mismatch"), None);
|
|
1288
1322
|
}
|
|
1289
1323
|
let pi_channel = observed.provider.eq_ignore_ascii_case("pi")
|
|
1290
1324
|
&& observed.evidence_method.as_deref() == Some("pi_activity_channel");
|
|
@@ -1321,6 +1355,7 @@ fn project_node_with_attach(
|
|
|
1321
1355
|
health: health.to_string(),
|
|
1322
1356
|
session_name: observed.session_name.clone(),
|
|
1323
1357
|
tmux_command,
|
|
1358
|
+
observation_reason: (activity == "unknown" || health == "unknown").then_some("activity_or_health_not_observed"),
|
|
1324
1359
|
},
|
|
1325
1360
|
attach,
|
|
1326
1361
|
)
|
|
@@ -1405,9 +1440,15 @@ impl BriefNode {
|
|
|
1405
1440
|
health: "unknown".to_string(),
|
|
1406
1441
|
session_name: None,
|
|
1407
1442
|
tmux_command: None,
|
|
1443
|
+
observation_reason: Some("observation_inconclusive"),
|
|
1408
1444
|
}
|
|
1409
1445
|
}
|
|
1410
1446
|
|
|
1447
|
+
fn with_reason(mut self, reason: &'static str) -> Self {
|
|
1448
|
+
self.observation_reason = Some(reason);
|
|
1449
|
+
self
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1411
1452
|
fn into_json(self) -> Value {
|
|
1412
1453
|
let mut object = Map::new();
|
|
1413
1454
|
object.insert("name".to_string(), json!(self.name));
|
|
@@ -1425,10 +1466,30 @@ impl BriefNode {
|
|
|
1425
1466
|
"tmux_command".to_string(),
|
|
1426
1467
|
self.tmux_command.map(Value::String).unwrap_or(Value::Null),
|
|
1427
1468
|
);
|
|
1469
|
+
if let Some(reason) = self.observation_reason {
|
|
1470
|
+
object.insert("observation_reason".into(), json!(reason));
|
|
1471
|
+
}
|
|
1428
1472
|
Value::Object(object)
|
|
1429
1473
|
}
|
|
1430
1474
|
}
|
|
1431
1475
|
|
|
1476
|
+
fn scoped_node_json(node: BriefNode, workspace: &Path, state: &Value) -> Value {
|
|
1477
|
+
let mut value = node.into_json();
|
|
1478
|
+
if let Some(reason) = value["observation_reason"].as_str() {
|
|
1479
|
+
let guidance = match reason {
|
|
1480
|
+
"pane_process_exited" => "pane process exit confirmed; inspect saved session and Leader binding before explicit start",
|
|
1481
|
+
"startup_not_yet_observed" | "native_runtime_not_ready" => "wait for startup convergence and recheck this scope; do not reset on unknown",
|
|
1482
|
+
_ => "inspect this scope's observation and identity; do not infer process exit or retry lifecycle from unknown alone",
|
|
1483
|
+
};
|
|
1484
|
+
value["next_action"] = json!(format!(
|
|
1485
|
+
"team-agent doctor --workspace {} --team {} --json; {guidance}",
|
|
1486
|
+
shell_quote(&workspace.to_string_lossy()),
|
|
1487
|
+
shell_quote(&crate::state::projection::team_state_key(state)),
|
|
1488
|
+
));
|
|
1489
|
+
}
|
|
1490
|
+
value
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1432
1493
|
#[cfg(test)]
|
|
1433
1494
|
mod native_ps_batch_regressions {
|
|
1434
1495
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
@@ -1448,6 +1509,35 @@ mod native_ps_batch_regressions {
|
|
|
1448
1509
|
}
|
|
1449
1510
|
}
|
|
1450
1511
|
|
|
1512
|
+
#[test]
|
|
1513
|
+
fn issue338_unknown_status_has_reason_and_same_scope_next_action() {
|
|
1514
|
+
let state = json!({
|
|
1515
|
+
"team_key": "scoped-team", "session_name": "sess",
|
|
1516
|
+
"agents": {"worker": {"provider": "pi", "status": "running"}}
|
|
1517
|
+
});
|
|
1518
|
+
let status = status_brief_scoped(Path::new("/owned/workspace with spaces"), &state, None);
|
|
1519
|
+
let node = &status["nodes"][0];
|
|
1520
|
+
assert_eq!(node["runtime_status"], "unknown");
|
|
1521
|
+
assert_eq!(node["observation_reason"], "endpoint_unregistered");
|
|
1522
|
+
let action = node["next_action"].as_str().unwrap();
|
|
1523
|
+
assert!(action.contains("--workspace '/owned/workspace with spaces'"));
|
|
1524
|
+
assert!(action.contains("--team 'scoped-team'"));
|
|
1525
|
+
assert!(action.contains("doctor"));
|
|
1526
|
+
assert!(!action.contains("restart"));
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
#[test]
|
|
1530
|
+
fn issue338_confirmed_zombie_is_not_reported_as_unexplained_unknown() {
|
|
1531
|
+
let node = registered("e1", "%7");
|
|
1532
|
+
let probe = sample_native_panes("hp\tw\t%7\t123\tpi\n", "e1", &[node.clone()], |_| {
|
|
1533
|
+
BTreeMap::from([(123, ProcessSnapshot { zombie: true })])
|
|
1534
|
+
});
|
|
1535
|
+
let value = project_node(&node, Some(&Some(probe))).into_json();
|
|
1536
|
+
assert_eq!(value["runtime_status"], "unknown");
|
|
1537
|
+
assert_eq!(value["observation_reason"], "pane_process_exited");
|
|
1538
|
+
assert!(value["tmux_command"].is_null());
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1451
1541
|
#[test]
|
|
1452
1542
|
fn eight_panes_use_one_endpoint_query() {
|
|
1453
1543
|
let registered = (0..8)
|
|
@@ -1713,6 +1803,7 @@ mod tests {
|
|
|
1713
1803
|
None,
|
|
1714
1804
|
Some(ProbeResult {
|
|
1715
1805
|
nodes: vec![probe("pi_activity_channel")],
|
|
1806
|
+
..Default::default()
|
|
1716
1807
|
}),
|
|
1717
1808
|
] {
|
|
1718
1809
|
let value = project_node(&node, Some(&sample)).into_json();
|
|
@@ -1724,7 +1815,11 @@ mod tests {
|
|
|
1724
1815
|
settings.0.unwrap_or("null"),
|
|
1725
1816
|
settings.1.unwrap_or("null")
|
|
1726
1817
|
)));
|
|
1727
|
-
|
|
1818
|
+
let diagnostics = usize::from(value.get("observation_reason").is_some());
|
|
1819
|
+
assert_eq!(value.as_object().unwrap().len(), 9 + diagnostics);
|
|
1820
|
+
if value["runtime_status"] == "unknown" {
|
|
1821
|
+
assert!(!value["observation_reason"].as_str().unwrap().is_empty());
|
|
1822
|
+
}
|
|
1728
1823
|
}
|
|
1729
1824
|
}
|
|
1730
1825
|
}
|
|
@@ -1757,6 +1852,7 @@ mod tests {
|
|
|
1757
1852
|
&node,
|
|
1758
1853
|
Some(&Some(ProbeResult {
|
|
1759
1854
|
nodes: vec![observed],
|
|
1855
|
+
..Default::default()
|
|
1760
1856
|
})),
|
|
1761
1857
|
);
|
|
1762
1858
|
assert_eq!(projected.runtime_status, "running");
|
|
@@ -1772,6 +1868,7 @@ mod tests {
|
|
|
1772
1868
|
&node,
|
|
1773
1869
|
Some(&Some(ProbeResult {
|
|
1774
1870
|
nodes: vec![observed],
|
|
1871
|
+
..Default::default()
|
|
1775
1872
|
})),
|
|
1776
1873
|
);
|
|
1777
1874
|
assert_eq!(projected.activity, "idle");
|
|
@@ -1800,6 +1897,7 @@ mod tests {
|
|
|
1800
1897
|
&node,
|
|
1801
1898
|
Some(&Some(ProbeResult {
|
|
1802
1899
|
nodes: vec![observed],
|
|
1900
|
+
..Default::default()
|
|
1803
1901
|
})),
|
|
1804
1902
|
);
|
|
1805
1903
|
assert_eq!(projected.runtime_status, "unknown");
|
|
@@ -1814,6 +1912,7 @@ mod tests {
|
|
|
1814
1912
|
&node,
|
|
1815
1913
|
Some(&Some(ProbeResult {
|
|
1816
1914
|
nodes: vec![probe("pi_activity_channel"), probe("pi_activity_channel")],
|
|
1915
|
+
..Default::default()
|
|
1817
1916
|
})),
|
|
1818
1917
|
);
|
|
1819
1918
|
assert_eq!(projected.runtime_status, "unknown");
|
|
@@ -1829,6 +1928,7 @@ mod tests {
|
|
|
1829
1928
|
&node,
|
|
1830
1929
|
Some(&Some(ProbeResult {
|
|
1831
1930
|
nodes: vec![observed],
|
|
1931
|
+
..Default::default()
|
|
1832
1932
|
})),
|
|
1833
1933
|
);
|
|
1834
1934
|
assert_eq!(projected.runtime_status, "unknown");
|
|
@@ -1841,6 +1941,7 @@ mod tests {
|
|
|
1841
1941
|
&node,
|
|
1842
1942
|
Some(&Some(ProbeResult {
|
|
1843
1943
|
nodes: vec![probe("pi_activity_channel"), probe("pi_activity_channel")],
|
|
1944
|
+
..Default::default()
|
|
1844
1945
|
})),
|
|
1845
1946
|
);
|
|
1846
1947
|
assert_eq!(projected.runtime_status, "unknown");
|
|
@@ -875,7 +875,12 @@ fn kiro_leader_entry_has_help_and_typed_refusal_without_native_or_fallback_effec
|
|
|
875
875
|
assert_eq!(value["reason"], "kiro_leader_not_admitted");
|
|
876
876
|
let mut argv = vec!["kiro".into()];
|
|
877
877
|
argv.extend(args);
|
|
878
|
-
|
|
878
|
+
let expected = if values.contains(&"--attach-existing") {
|
|
879
|
+
ExitCode::Usage // Unknown controls fail before capability dispatch.
|
|
880
|
+
} else {
|
|
881
|
+
ExitCode::Error // Supported syntax retains the typed refusal.
|
|
882
|
+
};
|
|
883
|
+
assert_eq!(crate::cli::run(&argv, &cwd), expected);
|
|
879
884
|
}
|
|
880
885
|
assert_eq!(std::fs::read_dir(&cwd).unwrap().count(), 0);
|
|
881
886
|
assert_eq!(
|