@team-agent/installer 0.5.52 → 0.5.54
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/crates/team-agent/src/cli/adapters.rs +18 -2
- package/crates/team-agent/src/cli/emit.rs +111 -3
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +12 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
- package/crates/team-agent/src/db/message_store.rs +31 -2
- package/crates/team-agent/src/db/migration.rs +7 -6
- package/crates/team-agent/src/db/schema.rs +18 -5
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +324 -57
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tools.rs +89 -4
- package/crates/team-agent/src/mcp_server/types.rs +7 -0
- package/crates/team-agent/src/mcp_server/wire.rs +65 -4
- package/crates/team-agent/src/messaging/delivery.rs +2 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +100 -21
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/persist.rs +68 -2
- package/crates/team-agent/src/messaging/presentation.rs +307 -0
- package/crates/team-agent/src/messaging/results.rs +130 -3
- package/crates/team-agent/src/messaging/selftest.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +54 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +85 -24
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/watchers.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +395 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +53 -92
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/schemas/result-envelope.schema.json +10 -0
- package/skills/team-agent/SKILL.md +11 -3
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -876,8 +876,10 @@ fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
}
|
|
879
|
-
crate::state::repository::StateRepository::new(workspace)
|
|
880
|
-
|
|
879
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
880
|
+
crate::state::repository::StateWriteIntent::FakeE2eSeed,
|
|
881
|
+
&state,
|
|
882
|
+
)?;
|
|
881
883
|
Ok(json!({
|
|
882
884
|
"ok": true,
|
|
883
885
|
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
@@ -1375,6 +1377,20 @@ pub fn cmd_fork_agent(args: &ForkAgentArgs) -> Result<CmdResult, CliError> {
|
|
|
1375
1377
|
))
|
|
1376
1378
|
}
|
|
1377
1379
|
|
|
1380
|
+
pub fn cmd_clone_agent(args: &CloneAgentArgs) -> Result<CmdResult, CliError> {
|
|
1381
|
+
Ok(CmdResult::from_json(
|
|
1382
|
+
lifecycle_port::clone_agent(
|
|
1383
|
+
&args.workspace,
|
|
1384
|
+
&args.source_agent,
|
|
1385
|
+
&args.as_agent,
|
|
1386
|
+
args.label.as_deref(),
|
|
1387
|
+
!args.no_display,
|
|
1388
|
+
args.team.as_deref(),
|
|
1389
|
+
)?,
|
|
1390
|
+
args.json,
|
|
1391
|
+
))
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1378
1394
|
/// `cmd_remove_agent`(`commands.py:394`)。
|
|
1379
1395
|
pub fn cmd_remove_agent(args: &RemoveAgentArgs) -> Result<CmdResult, CliError> {
|
|
1380
1396
|
Ok(CmdResult::from_json(
|
|
@@ -115,6 +115,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
115
115
|
"stop-agent" => cmd_stop_agent(&stop_agent_args(args, cwd)?).map(emit_result),
|
|
116
116
|
"reset-agent" => cmd_reset_agent(&reset_agent_args(args, cwd)?).map(emit_result),
|
|
117
117
|
"add-agent" => cmd_add_agent(&add_agent_args(args, cwd)?).map(emit_result),
|
|
118
|
+
"clone-agent" => cmd_clone_agent(&clone_agent_args(args, cwd)?).map(emit_result),
|
|
118
119
|
"fork-agent" => cmd_fork_agent(&fork_agent_args(args, cwd)?).map(emit_result),
|
|
119
120
|
"remove-agent" => cmd_remove_agent(&remove_agent_args(args, cwd)?).map(emit_result),
|
|
120
121
|
"stuck-list" => cmd_stuck_list(&stuck_list_args(args, cwd)).map(emit_result),
|
|
@@ -316,8 +317,11 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
316
317
|
Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
|
|
317
318
|
Some("send") => concat!(
|
|
318
319
|
"usage: team-agent send TO MESSAGE... ",
|
|
319
|
-
"[--workspace WORKSPACE] [--team TEAM]
|
|
320
|
-
"
|
|
320
|
+
"[--workspace WORKSPACE] [--team TEAM] ",
|
|
321
|
+
"[--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] ",
|
|
322
|
+
"[--json]\n\n",
|
|
323
|
+
"TO is a logical recipient; send returns after the message is persisted. ",
|
|
324
|
+
"Presentation changes live display only; every sink remains durable."
|
|
321
325
|
)
|
|
322
326
|
.to_string(),
|
|
323
327
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
@@ -330,6 +334,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
330
334
|
Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]".to_string(),
|
|
331
335
|
Some("stop-agent") => "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
332
336
|
Some("add-agent") => "usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
|
|
337
|
+
Some("clone-agent") => "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
|
|
333
338
|
Some("fork-agent") => "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
|
|
334
339
|
Some("remove-agent") => "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]".to_string(),
|
|
335
340
|
// 0.5.26 (§7.6): removed from help; dispatch was never wired.
|
|
@@ -773,6 +778,9 @@ struct ParsedArgs {
|
|
|
773
778
|
socket: Option<String>,
|
|
774
779
|
thread_id: Option<String>,
|
|
775
780
|
message_id: Option<String>,
|
|
781
|
+
presentation_sink: Option<String>,
|
|
782
|
+
message_class: Option<String>,
|
|
783
|
+
case_id: Option<String>,
|
|
776
784
|
content: Option<String>,
|
|
777
785
|
primary_error: Option<String>,
|
|
778
786
|
agent_id: Option<String>,
|
|
@@ -863,6 +871,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
863
871
|
"--socket" => parsed.socket = next_arg(args, &mut i),
|
|
864
872
|
"--thread-id" => parsed.thread_id = next_arg(args, &mut i),
|
|
865
873
|
"--message-id" => parsed.message_id = next_arg(args, &mut i),
|
|
874
|
+
"--presentation-sink" => parsed.presentation_sink = next_arg(args, &mut i),
|
|
875
|
+
"--message-class" => parsed.message_class = next_arg(args, &mut i),
|
|
876
|
+
"--case-id" => parsed.case_id = next_arg(args, &mut i),
|
|
866
877
|
"--content" => parsed.content = next_arg(args, &mut i),
|
|
867
878
|
"--primary-error" => parsed.primary_error = next_arg(args, &mut i),
|
|
868
879
|
"--result-json" => parsed.result_json = next_arg(args, &mut i),
|
|
@@ -879,6 +890,17 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
879
890
|
other if other.starts_with("--to-leader=") => {
|
|
880
891
|
parsed.to_leader = Some(other.trim_start_matches("--to-leader=").to_string());
|
|
881
892
|
}
|
|
893
|
+
other if other.starts_with("--presentation-sink=") => {
|
|
894
|
+
parsed.presentation_sink =
|
|
895
|
+
Some(other.trim_start_matches("--presentation-sink=").to_string());
|
|
896
|
+
}
|
|
897
|
+
other if other.starts_with("--message-class=") => {
|
|
898
|
+
parsed.message_class =
|
|
899
|
+
Some(other.trim_start_matches("--message-class=").to_string());
|
|
900
|
+
}
|
|
901
|
+
other if other.starts_with("--case-id=") => {
|
|
902
|
+
parsed.case_id = Some(other.trim_start_matches("--case-id=").to_string());
|
|
903
|
+
}
|
|
882
904
|
other if other.starts_with("--provider=") => {
|
|
883
905
|
parsed.provider = Some(other.trim_start_matches("--provider=").to_string());
|
|
884
906
|
}
|
|
@@ -1027,6 +1049,23 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
1027
1049
|
};
|
|
1028
1050
|
let message_start = usize::from(target.is_some());
|
|
1029
1051
|
let workspace = workspace(&parsed, cwd);
|
|
1052
|
+
let presentation_value = if parsed.presentation_sink.is_some()
|
|
1053
|
+
|| parsed.message_class.is_some()
|
|
1054
|
+
|| parsed.case_id.is_some()
|
|
1055
|
+
{
|
|
1056
|
+
Some(serde_json::json!({
|
|
1057
|
+
"sink": parsed.presentation_sink.clone(),
|
|
1058
|
+
"class": parsed.message_class.clone(),
|
|
1059
|
+
"case_id": parsed.case_id.clone(),
|
|
1060
|
+
}))
|
|
1061
|
+
} else {
|
|
1062
|
+
None
|
|
1063
|
+
};
|
|
1064
|
+
let (presentation, presentation_error) =
|
|
1065
|
+
crate::messaging::presentation::normalize_presentation(presentation_value.as_ref());
|
|
1066
|
+
if let Some(error) = presentation_error {
|
|
1067
|
+
return Err(CliError::Usage(format!("invalid presentation: {error}")));
|
|
1068
|
+
}
|
|
1030
1069
|
Ok(SendArgs {
|
|
1031
1070
|
target,
|
|
1032
1071
|
message: parsed
|
|
@@ -1047,6 +1086,7 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
|
|
|
1047
1086
|
confirm_human: false,
|
|
1048
1087
|
json: parsed.json,
|
|
1049
1088
|
message_id: None,
|
|
1089
|
+
presentation,
|
|
1050
1090
|
pane: parsed.pane.clone(),
|
|
1051
1091
|
to_name: parsed.to_name.clone(),
|
|
1052
1092
|
to_leader: parsed.to_leader.clone(),
|
|
@@ -1109,11 +1149,22 @@ fn validate_send_flags(args: &[String]) -> Result<(), CliError> {
|
|
|
1109
1149
|
"--timeout",
|
|
1110
1150
|
"--confirm-human",
|
|
1111
1151
|
"--message-id",
|
|
1152
|
+
"--presentation-sink",
|
|
1153
|
+
"--message-class",
|
|
1154
|
+
"--case-id",
|
|
1112
1155
|
"--json",
|
|
1113
1156
|
"-h",
|
|
1114
1157
|
"--help",
|
|
1115
1158
|
];
|
|
1116
|
-
const ALLOWED_PREFIXES: &[&str] = &[
|
|
1159
|
+
const ALLOWED_PREFIXES: &[&str] = &[
|
|
1160
|
+
"--team=",
|
|
1161
|
+
"--pane=",
|
|
1162
|
+
"--to-name=",
|
|
1163
|
+
"--to-leader=",
|
|
1164
|
+
"--presentation-sink=",
|
|
1165
|
+
"--message-class=",
|
|
1166
|
+
"--case-id=",
|
|
1167
|
+
];
|
|
1117
1168
|
if let Some(flag) = args.iter().find(|arg| {
|
|
1118
1169
|
arg.starts_with('-')
|
|
1119
1170
|
&& !ALLOWED.contains(&arg.as_str())
|
|
@@ -1366,6 +1417,21 @@ fn fork_agent_args(args: &[String], cwd: &Path) -> Result<ForkAgentArgs, CliErro
|
|
|
1366
1417
|
})
|
|
1367
1418
|
}
|
|
1368
1419
|
|
|
1420
|
+
fn clone_agent_args(args: &[String], cwd: &Path) -> Result<CloneAgentArgs, CliError> {
|
|
1421
|
+
let parsed = parse_args(args);
|
|
1422
|
+
Ok(CloneAgentArgs {
|
|
1423
|
+
source_agent: required_pos(&parsed, 0, "source_agent")?,
|
|
1424
|
+
workspace: workspace(&parsed, cwd),
|
|
1425
|
+
team: parsed.team,
|
|
1426
|
+
as_agent: parsed
|
|
1427
|
+
.as_agent
|
|
1428
|
+
.ok_or_else(|| CliError::Usage("missing --as".to_string()))?,
|
|
1429
|
+
label: parsed.label,
|
|
1430
|
+
no_display: parsed.no_display,
|
|
1431
|
+
json: parsed.json,
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1369
1435
|
fn remove_agent_args(args: &[String], cwd: &Path) -> Result<RemoveAgentArgs, CliError> {
|
|
1370
1436
|
let parsed = parse_args(args);
|
|
1371
1437
|
let workspace = workspace(&parsed, cwd);
|
|
@@ -1998,6 +2064,48 @@ mod tests {
|
|
|
1998
2064
|
let _ = std::fs::remove_dir_all(&cwd);
|
|
1999
2065
|
}
|
|
2000
2066
|
|
|
2067
|
+
#[test]
|
|
2068
|
+
fn send_presentation_flags_map_to_the_typed_request() {
|
|
2069
|
+
let cwd = tmp_workspace();
|
|
2070
|
+
let args = send_args(
|
|
2071
|
+
&cli_argv(&[
|
|
2072
|
+
"leader",
|
|
2073
|
+
"progress",
|
|
2074
|
+
"--presentation-sink",
|
|
2075
|
+
"casefile",
|
|
2076
|
+
"--message-class",
|
|
2077
|
+
"progress",
|
|
2078
|
+
"--case-id",
|
|
2079
|
+
"case-9",
|
|
2080
|
+
]),
|
|
2081
|
+
&cwd,
|
|
2082
|
+
)
|
|
2083
|
+
.unwrap();
|
|
2084
|
+
assert_eq!(
|
|
2085
|
+
args.presentation.sink,
|
|
2086
|
+
crate::messaging::presentation::PresentationSink::Casefile
|
|
2087
|
+
);
|
|
2088
|
+
assert_eq!(
|
|
2089
|
+
args.presentation.class,
|
|
2090
|
+
crate::messaging::presentation::PresentationClass::Progress
|
|
2091
|
+
);
|
|
2092
|
+
assert_eq!(args.presentation.case_id.as_deref(), Some("case-9"));
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
#[test]
|
|
2096
|
+
fn send_presentation_flags_fail_closed_when_incomplete() {
|
|
2097
|
+
let cwd = tmp_workspace();
|
|
2098
|
+
let error = send_args(
|
|
2099
|
+
&cli_argv(&["leader", "progress", "--presentation-sink", "casefile"]),
|
|
2100
|
+
&cwd,
|
|
2101
|
+
)
|
|
2102
|
+
.unwrap_err();
|
|
2103
|
+
assert!(matches!(
|
|
2104
|
+
error,
|
|
2105
|
+
CliError::Usage(message) if message == "invalid presentation: missing_class"
|
|
2106
|
+
));
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2001
2109
|
#[test]
|
|
2002
2110
|
fn send_to_name_positionals_are_message_not_target() {
|
|
2003
2111
|
let cwd = tmp_workspace();
|
|
@@ -374,6 +374,19 @@ pub mod lifecycle_port {
|
|
|
374
374
|
);
|
|
375
375
|
let mut error = None;
|
|
376
376
|
for session in &sessions {
|
|
377
|
+
let targets = vec![session.as_str().to_string()];
|
|
378
|
+
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
379
|
+
event_log,
|
|
380
|
+
transport,
|
|
381
|
+
"shutdown.shared_socket_individual",
|
|
382
|
+
crate::kill_audit::KILL_SESSION,
|
|
383
|
+
&targets,
|
|
384
|
+
) {
|
|
385
|
+
error.get_or_insert_with(|| {
|
|
386
|
+
format!("pre-kill audit failed: {audit_error}")
|
|
387
|
+
});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
377
390
|
if let Err(err) = transport.kill_session(session) {
|
|
378
391
|
if !tmux_absent_error(&err.to_string()) {
|
|
379
392
|
error.get_or_insert_with(|| err.to_string());
|
|
@@ -386,7 +399,20 @@ pub mod lifecycle_port {
|
|
|
386
399
|
error,
|
|
387
400
|
};
|
|
388
401
|
}
|
|
389
|
-
let
|
|
402
|
+
let targets = sessions
|
|
403
|
+
.iter()
|
|
404
|
+
.map(|session| session.as_str().to_string())
|
|
405
|
+
.collect::<Vec<_>>();
|
|
406
|
+
let error = match crate::kill_audit::pre_kill_audit(
|
|
407
|
+
event_log,
|
|
408
|
+
transport,
|
|
409
|
+
"shutdown.exclusive_socket",
|
|
410
|
+
crate::kill_audit::KILL_SERVER,
|
|
411
|
+
&targets,
|
|
412
|
+
) {
|
|
413
|
+
Ok(()) => transport.kill_server().err().map(|error| error.to_string()),
|
|
414
|
+
Err(error) => Some(format!("pre-kill audit failed: {error}")),
|
|
415
|
+
};
|
|
390
416
|
ShutdownSocketCleanup {
|
|
391
417
|
killed_sessions: sessions,
|
|
392
418
|
spared_sessions: Vec::new(),
|
|
@@ -406,6 +432,18 @@ pub mod lifecycle_port {
|
|
|
406
432
|
}
|
|
407
433
|
let mut error = None;
|
|
408
434
|
for session in &to_kill {
|
|
435
|
+
let targets = vec![session.as_str().to_string()];
|
|
436
|
+
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
437
|
+
event_log,
|
|
438
|
+
transport,
|
|
439
|
+
"shutdown.selected_session",
|
|
440
|
+
crate::kill_audit::KILL_SESSION,
|
|
441
|
+
&targets,
|
|
442
|
+
) {
|
|
443
|
+
error
|
|
444
|
+
.get_or_insert_with(|| format!("pre-kill audit failed: {audit_error}"));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
409
447
|
if let Err(err) = transport.kill_session(session) {
|
|
410
448
|
if !tmux_absent_error(&err.to_string()) {
|
|
411
449
|
error.get_or_insert_with(|| err.to_string());
|
|
@@ -421,6 +459,59 @@ pub mod lifecycle_port {
|
|
|
421
459
|
}
|
|
422
460
|
}
|
|
423
461
|
|
|
462
|
+
#[cfg(test)]
|
|
463
|
+
mod kill_audit_tests {
|
|
464
|
+
use std::collections::BTreeMap;
|
|
465
|
+
|
|
466
|
+
use super::*;
|
|
467
|
+
use crate::transport::test_support::OfflineTransport;
|
|
468
|
+
use crate::transport::{PaneId, PaneInfo, SessionName};
|
|
469
|
+
|
|
470
|
+
#[test]
|
|
471
|
+
fn audit_write_failure_skips_destructive_kill() {
|
|
472
|
+
let root =
|
|
473
|
+
std::env::temp_dir().join(format!("ta-kill-audit-fail-{}", std::process::id()));
|
|
474
|
+
let blocked_parent = root.join("not-a-directory");
|
|
475
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
476
|
+
std::fs::write(&blocked_parent, b"file").unwrap();
|
|
477
|
+
let event_log = crate::event_log::EventLog::at(blocked_parent.join("events.jsonl"));
|
|
478
|
+
let session = SessionName::new("team-audit-fail");
|
|
479
|
+
let transport = OfflineTransport::new()
|
|
480
|
+
.with_session_present(true)
|
|
481
|
+
.with_targets(vec![PaneInfo {
|
|
482
|
+
pane_id: PaneId::new("%1"),
|
|
483
|
+
session: session.clone(),
|
|
484
|
+
window_index: Some(0),
|
|
485
|
+
window_name: None,
|
|
486
|
+
pane_index: Some(0),
|
|
487
|
+
tty: None,
|
|
488
|
+
current_command: None,
|
|
489
|
+
current_path: None,
|
|
490
|
+
active: true,
|
|
491
|
+
pane_pid: None,
|
|
492
|
+
leader_env: BTreeMap::new(),
|
|
493
|
+
}]);
|
|
494
|
+
let cleanup = bare_shutdown_socket_cleanup(
|
|
495
|
+
&transport,
|
|
496
|
+
&json!({
|
|
497
|
+
"is_external_leader": true,
|
|
498
|
+
"tmux_socket_source": "leader_env",
|
|
499
|
+
}),
|
|
500
|
+
&event_log,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
assert!(
|
|
504
|
+
!transport.calls().contains(&"kill_session"),
|
|
505
|
+
"an unaudited destructive call must not execute"
|
|
506
|
+
);
|
|
507
|
+
assert!(cleanup
|
|
508
|
+
.error
|
|
509
|
+
.as_deref()
|
|
510
|
+
.is_some_and(|error| error.contains("pre-kill audit failed")));
|
|
511
|
+
let _ = std::fs::remove_dir_all(root);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
424
515
|
fn state_uses_external_leader(state: &Value) -> bool {
|
|
425
516
|
crate::state::projection::state_is_external_leader(state)
|
|
426
517
|
}
|
|
@@ -501,6 +592,17 @@ pub mod lifecycle_port {
|
|
|
501
592
|
);
|
|
502
593
|
continue;
|
|
503
594
|
}
|
|
595
|
+
let targets = vec![session.as_str().to_string()];
|
|
596
|
+
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
597
|
+
event_log,
|
|
598
|
+
transport,
|
|
599
|
+
"shutdown.managed_worker_session",
|
|
600
|
+
crate::kill_audit::KILL_SESSION,
|
|
601
|
+
&targets,
|
|
602
|
+
) {
|
|
603
|
+
error.get_or_insert_with(|| format!("pre-kill audit failed: {audit_error}"));
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
504
606
|
if let Err(err) = transport.kill_session(session) {
|
|
505
607
|
if !tmux_absent_error(&err.to_string()) {
|
|
506
608
|
error.get_or_insert_with(|| err.to_string());
|
|
@@ -598,6 +700,16 @@ pub mod lifecycle_port {
|
|
|
598
700
|
);
|
|
599
701
|
return cleanup;
|
|
600
702
|
}
|
|
703
|
+
if let Err(error) = crate::kill_audit::pre_kill_audit(
|
|
704
|
+
event_log,
|
|
705
|
+
transport,
|
|
706
|
+
"shutdown.empty_owned_endpoint",
|
|
707
|
+
crate::kill_audit::KILL_SERVER,
|
|
708
|
+
&[],
|
|
709
|
+
) {
|
|
710
|
+
cleanup.error = Some(format!("pre-kill audit failed: {error}"));
|
|
711
|
+
return cleanup;
|
|
712
|
+
}
|
|
601
713
|
if let Err(error) = transport.kill_server() {
|
|
602
714
|
cleanup.error = Some(error.to_string());
|
|
603
715
|
}
|
|
@@ -748,11 +860,24 @@ pub mod lifecycle_port {
|
|
|
748
860
|
let _ =
|
|
749
861
|
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
750
862
|
} else {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
863
|
+
let targets = vec![session.as_str().to_string()];
|
|
864
|
+
let event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
865
|
+
if let Err(error) = crate::kill_audit::pre_kill_audit(
|
|
866
|
+
&event_log,
|
|
867
|
+
transport,
|
|
868
|
+
"shutdown.team_session",
|
|
869
|
+
crate::kill_audit::KILL_SESSION,
|
|
870
|
+
&targets,
|
|
871
|
+
) {
|
|
872
|
+
kill_error = Some(format!("pre-kill audit failed: {error}"));
|
|
873
|
+
} else if let Err(error) = transport.kill_session(session) {
|
|
874
|
+
if tmux_absent_error(&error.to_string()) {
|
|
875
|
+
push_unique_session(&mut killed_sessions, session.clone());
|
|
876
|
+
} else {
|
|
754
877
|
kill_error = Some(error.to_string());
|
|
755
878
|
}
|
|
879
|
+
} else {
|
|
880
|
+
push_unique_session(&mut killed_sessions, session.clone());
|
|
756
881
|
}
|
|
757
882
|
let _ =
|
|
758
883
|
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
@@ -2357,10 +2482,36 @@ pub mod lifecycle_port {
|
|
|
2357
2482
|
"ok": true,
|
|
2358
2483
|
"source_agent_id": report.source_agent_id.as_str(),
|
|
2359
2484
|
"new_agent_id": report.new_agent_id.as_str(),
|
|
2485
|
+
"session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2486
|
+
"new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2360
2487
|
})),
|
|
2361
2488
|
Err(e) => Ok(error_value(e)),
|
|
2362
2489
|
}
|
|
2363
2490
|
}
|
|
2491
|
+
pub fn clone_agent(
|
|
2492
|
+
workspace: &Path,
|
|
2493
|
+
source_agent: &str,
|
|
2494
|
+
as_agent_id: &str,
|
|
2495
|
+
label: Option<&str>,
|
|
2496
|
+
open_display: bool,
|
|
2497
|
+
team: Option<&str>,
|
|
2498
|
+
) -> Result<Value, CliError> {
|
|
2499
|
+
let source = crate::model::ids::AgentId::new(source_agent);
|
|
2500
|
+
let dest = crate::model::ids::AgentId::new(as_agent_id);
|
|
2501
|
+
match crate::lifecycle::clone_agent(workspace, &source, &dest, label, open_display, team) {
|
|
2502
|
+
Ok(report) => Ok(json!({
|
|
2503
|
+
"ok": true,
|
|
2504
|
+
"status": "cloned",
|
|
2505
|
+
"source_agent_id": report.source_agent_id.as_str(),
|
|
2506
|
+
"new_agent_id": report.new_agent_id.as_str(),
|
|
2507
|
+
"session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2508
|
+
"new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2509
|
+
"backing_path": report.backing_path.as_ref().map(|path| path.to_string_lossy()),
|
|
2510
|
+
"backing_state": report.backing_state,
|
|
2511
|
+
})),
|
|
2512
|
+
Err(error) => Ok(error_value(error)),
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2364
2515
|
/// `runtime.remove_agent`(`cmd_remove_agent`;`--from-spec` 须配 `--confirm`)。
|
|
2365
2516
|
pub fn remove_agent(
|
|
2366
2517
|
workspace: &Path,
|
|
@@ -25,6 +25,7 @@ pub fn send_options_from_args(args: &SendArgs) -> SendOptions {
|
|
|
25
25
|
watch_result: args.watch_result,
|
|
26
26
|
team: args.team.as_ref().map(|s| TeamKey::new(s.clone())),
|
|
27
27
|
message_id: args.message_id.clone(),
|
|
28
|
+
presentation: args.presentation.clone(),
|
|
28
29
|
..SendOptions::default()
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -285,6 +285,7 @@ pub(super) fn first_target(target: &MessageTarget) -> String {
|
|
|
285
285
|
pub(super) fn delivery_status_wire(status: DeliveryStatus) -> &'static str {
|
|
286
286
|
match status {
|
|
287
287
|
DeliveryStatus::Delivered => "delivered",
|
|
288
|
+
DeliveryStatus::StoredOnly => "stored_only",
|
|
288
289
|
DeliveryStatus::Failed => "failed",
|
|
289
290
|
DeliveryStatus::Queued => "queued",
|
|
290
291
|
DeliveryStatus::Blocked => "blocked",
|
|
@@ -55,6 +55,7 @@ pub(crate) enum DispatchKind {
|
|
|
55
55
|
StopAgent,
|
|
56
56
|
ResetAgent,
|
|
57
57
|
AddAgent,
|
|
58
|
+
CloneAgent,
|
|
58
59
|
ForkAgent,
|
|
59
60
|
RemoveAgent,
|
|
60
61
|
StuckList,
|
|
@@ -98,6 +99,7 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
|
|
|
98
99
|
DispatchKind::StopAgent,
|
|
99
100
|
DispatchKind::ResetAgent,
|
|
100
101
|
DispatchKind::AddAgent,
|
|
102
|
+
DispatchKind::CloneAgent,
|
|
101
103
|
DispatchKind::ForkAgent,
|
|
102
104
|
DispatchKind::RemoveAgent,
|
|
103
105
|
DispatchKind::StuckList,
|
|
@@ -153,7 +155,7 @@ pub(crate) struct CommandSpec {
|
|
|
153
155
|
#[rustfmt::skip]
|
|
154
156
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
155
157
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
156
|
-
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for a logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
158
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for a logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
157
159
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
158
160
|
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
159
161
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -183,6 +185,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
183
185
|
CommandSpec { name: "attach-app-server-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::AttachAppServerLeader), summary: "bind an app-server leader receiver", usage: "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
184
186
|
CommandSpec { name: "remove-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "remove a worker", usage: "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
185
187
|
CommandSpec { name: "fork-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "fork a worker", usage: "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
188
|
+
CommandSpec { name: "clone-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "clone a worker role into a fresh session", usage: "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
186
189
|
CommandSpec { name: "allow-peer-talk", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::AllowPeerTalk), summary: "allow direct worker peer talk", usage: "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability/config surface" }) },
|
|
187
190
|
CommandSpec { name: "approvals", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "inspect provider approval state", usage: "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "approval diagnostics" }) },
|
|
188
191
|
CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
|
|
@@ -361,7 +361,7 @@ fn cleanup_orphans_dryrun_golden_envelope() {
|
|
|
361
361
|
// ── fix_schema: golden fix_schema_layout diagnosis envelope (schema_migration.py:258) ─────────────
|
|
362
362
|
// {ok,status,db_path,schema_version,user_version,layout_diffs,recommended_action,would_backup_path,
|
|
363
363
|
// fixed,rebuilds}. RUST mod.rs:538-540 stub {ok,fixed:false}. db_path/would_backup_path are
|
|
364
|
-
// path/clock-derived; lock the deterministic fields
|
|
364
|
+
// path/clock-derived; lock the deterministic fields against the canonical SCHEMA_VERSION. ────────
|
|
365
365
|
#[test]
|
|
366
366
|
fn fix_schema_golden_layout_diagnosis_envelope() {
|
|
367
367
|
let ws = tmp_workspace();
|
|
@@ -370,8 +370,8 @@ fn fix_schema_golden_layout_diagnosis_envelope() {
|
|
|
370
370
|
assert_eq!(v["status"], json!("missing"), "missing db: status missing");
|
|
371
371
|
assert_eq!(
|
|
372
372
|
v["schema_version"],
|
|
373
|
-
json!(
|
|
374
|
-
"golden schema_version == SCHEMA_VERSION
|
|
373
|
+
json!(crate::db::schema::SCHEMA_VERSION),
|
|
374
|
+
"golden schema_version == canonical SCHEMA_VERSION"
|
|
375
375
|
);
|
|
376
376
|
assert_eq!(v["user_version"], json!(0), "missing db user_version == 0");
|
|
377
377
|
assert_eq!(
|
|
@@ -157,6 +157,7 @@ fn named_send_args(
|
|
|
157
157
|
confirm_human: false,
|
|
158
158
|
json: true,
|
|
159
159
|
message_id: None,
|
|
160
|
+
presentation: crate::messaging::presentation::PresentationRequest::default(),
|
|
160
161
|
pane: pane.map(str::to_string),
|
|
161
162
|
to_name: to_name.map(str::to_string),
|
|
162
163
|
to_leader: None,
|
|
@@ -323,6 +323,7 @@ pub struct SendArgs {
|
|
|
323
323
|
/// When set, the store insert uses this id verbatim; a repeat with the same
|
|
324
324
|
/// id returns a `Duplicate` refusal instead of creating a second row.
|
|
325
325
|
pub message_id: Option<String>,
|
|
326
|
+
pub presentation: crate::messaging::presentation::PresentationRequest,
|
|
326
327
|
/// Deprecated compatibility input. Public send refuses pane identity and
|
|
327
328
|
/// requires a logical recipient so persistence always precedes delivery.
|
|
328
329
|
pub pane: Option<String>,
|
|
@@ -514,6 +515,17 @@ pub struct ForkAgentArgs {
|
|
|
514
515
|
pub json: bool,
|
|
515
516
|
}
|
|
516
517
|
|
|
518
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
519
|
+
pub struct CloneAgentArgs {
|
|
520
|
+
pub source_agent: String,
|
|
521
|
+
pub workspace: PathBuf,
|
|
522
|
+
pub team: Option<String>,
|
|
523
|
+
pub as_agent: String,
|
|
524
|
+
pub label: Option<String>,
|
|
525
|
+
pub no_display: bool,
|
|
526
|
+
pub json: bool,
|
|
527
|
+
}
|
|
528
|
+
|
|
517
529
|
/// `remove-agent`(`parser.py:414`)。`--from-spec` 须配 `--confirm`。
|
|
518
530
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
519
531
|
pub struct RemoveAgentArgs {
|
|
@@ -365,16 +365,16 @@ fn orphan_self_terminate_false_when_workspace_exists() {
|
|
|
365
365
|
|
|
366
366
|
#[test]
|
|
367
367
|
fn metadata_ok_requires_all_three_to_match() {
|
|
368
|
-
// metadata.py:37-43 — pid
|
|
369
|
-
let good = meta(555, PROTOCOL_VERSION,
|
|
368
|
+
// metadata.py:37-43 — pid, protocol version, and current message schema all match.
|
|
369
|
+
let good = meta(555, PROTOCOL_VERSION, crate::db::schema::SCHEMA_VERSION);
|
|
370
370
|
assert!(coordinator_metadata_ok(Some(&good), Pid(555)));
|
|
371
371
|
// pid 不符 → false。
|
|
372
372
|
assert!(!coordinator_metadata_ok(Some(&good), Pid(999)));
|
|
373
373
|
// protocol_version 不符(bump 触发 restart_incompatible)→ false。
|
|
374
|
-
let bad_proto = meta(555, 1,
|
|
374
|
+
let bad_proto = meta(555, 1, crate::db::schema::SCHEMA_VERSION);
|
|
375
375
|
assert!(!coordinator_metadata_ok(Some(&bad_proto), Pid(555)));
|
|
376
376
|
// schema_version 不符 → false(不可静默继续旧 schema 写库,card §89)。
|
|
377
|
-
let bad_schema = meta(555, PROTOCOL_VERSION,
|
|
377
|
+
let bad_schema = meta(555, PROTOCOL_VERSION, crate::db::schema::SCHEMA_VERSION - 1);
|
|
378
378
|
assert!(!coordinator_metadata_ok(Some(&bad_schema), Pid(555)));
|
|
379
379
|
}
|
|
380
380
|
|