@team-agent/installer 0.5.6 → 0.5.7
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/mod.rs +130 -1
- package/crates/team-agent/src/cli/send.rs +95 -5
- package/crates/team-agent/src/cli/tests/main_preserved.rs +54 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +109 -2
- package/crates/team-agent/src/lifecycle/restart/remove.rs +94 -15
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/types.rs +15 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -2314,12 +2314,29 @@ pub mod lifecycle_port {
|
|
|
2314
2314
|
force: bool,
|
|
2315
2315
|
team: Option<&str>,
|
|
2316
2316
|
) -> Result<Value, CliError> {
|
|
2317
|
+
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
2318
|
+
match crate::lifecycle::remove_agent_flag_requirements(workspace, &agent_id, team) {
|
|
2319
|
+
Ok(requirements) => {
|
|
2320
|
+
if !remove_agent_missing_flags(from_spec, confirm, force, &requirements).is_empty() {
|
|
2321
|
+
return Ok(remove_agent_flag_refusal(
|
|
2322
|
+
workspace,
|
|
2323
|
+
agent,
|
|
2324
|
+
team,
|
|
2325
|
+
from_spec,
|
|
2326
|
+
confirm,
|
|
2327
|
+
force,
|
|
2328
|
+
&requirements,
|
|
2329
|
+
));
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
Err(error) if confirm => return Ok(error_value(error)),
|
|
2333
|
+
Err(_) => {}
|
|
2334
|
+
}
|
|
2317
2335
|
if !confirm {
|
|
2318
2336
|
return Ok(
|
|
2319
2337
|
json!({"ok": false, "agent_id": agent, "error": "remove-agent requires --confirm"}),
|
|
2320
2338
|
);
|
|
2321
2339
|
}
|
|
2322
|
-
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
2323
2340
|
match crate::lifecycle::remove_agent(workspace, &agent_id, from_spec, force, team) {
|
|
2324
2341
|
Ok(report @ crate::lifecycle::RemoveAgentOutcome::Removed { .. }) => Ok(json!({
|
|
2325
2342
|
"ok": true,
|
|
@@ -2343,9 +2360,121 @@ pub mod lifecycle_port {
|
|
|
2343
2360
|
"error": "agent is running; remove-agent requires --force",
|
|
2344
2361
|
"action": "rerun with --force to stop and remove the running agent",
|
|
2345
2362
|
})),
|
|
2363
|
+
Ok(crate::lifecycle::RemoveAgentOutcome::RefusedRequiredFlags { .. }) => Ok(json!({
|
|
2364
|
+
"ok": false,
|
|
2365
|
+
"agent_id": agent,
|
|
2366
|
+
"status": "refused",
|
|
2367
|
+
"reason": "remove_agent_flags_required",
|
|
2368
|
+
"error": "remove-agent required flags changed; rerun the command from the latest refusal",
|
|
2369
|
+
})),
|
|
2346
2370
|
Err(e) => Ok(error_value(e)),
|
|
2347
2371
|
}
|
|
2348
2372
|
}
|
|
2373
|
+
|
|
2374
|
+
fn remove_agent_flag_refusal(
|
|
2375
|
+
workspace: &Path,
|
|
2376
|
+
agent: &str,
|
|
2377
|
+
team: Option<&str>,
|
|
2378
|
+
from_spec: bool,
|
|
2379
|
+
confirm: bool,
|
|
2380
|
+
force: bool,
|
|
2381
|
+
requirements: &crate::lifecycle::RemoveAgentFlagRequirements,
|
|
2382
|
+
) -> Value {
|
|
2383
|
+
let required_flags = remove_agent_required_flags(requirements);
|
|
2384
|
+
let missing_flags = remove_agent_missing_flags(from_spec, confirm, force, requirements);
|
|
2385
|
+
let reason = if missing_flags.len() == 1 {
|
|
2386
|
+
match missing_flags[0] {
|
|
2387
|
+
"--confirm" => "confirm_required",
|
|
2388
|
+
"--from-spec" => "from_spec_confirm_required",
|
|
2389
|
+
"--force" => "force_required",
|
|
2390
|
+
_ => "remove_agent_flags_required",
|
|
2391
|
+
}
|
|
2392
|
+
} else {
|
|
2393
|
+
"remove_agent_flags_required"
|
|
2394
|
+
};
|
|
2395
|
+
let command = remove_agent_command(workspace, agent, team, &required_flags);
|
|
2396
|
+
let required = required_flags.join(" ");
|
|
2397
|
+
json!({
|
|
2398
|
+
"ok": false,
|
|
2399
|
+
"agent_id": agent,
|
|
2400
|
+
"status": "refused",
|
|
2401
|
+
"reason": reason,
|
|
2402
|
+
"error": format!("remove-agent requires {required} for this agent"),
|
|
2403
|
+
"action": format!("rerun: {command}"),
|
|
2404
|
+
"command": command,
|
|
2405
|
+
"missing_flags": missing_flags,
|
|
2406
|
+
"required_flags": required_flags,
|
|
2407
|
+
"state": {
|
|
2408
|
+
"from_spec_required": requirements.from_spec_required,
|
|
2409
|
+
"running": requirements.force_required,
|
|
2410
|
+
"has_session": requirements.has_session,
|
|
2411
|
+
},
|
|
2412
|
+
})
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
fn remove_agent_missing_flags(
|
|
2416
|
+
from_spec: bool,
|
|
2417
|
+
confirm: bool,
|
|
2418
|
+
force: bool,
|
|
2419
|
+
requirements: &crate::lifecycle::RemoveAgentFlagRequirements,
|
|
2420
|
+
) -> Vec<&'static str> {
|
|
2421
|
+
remove_agent_required_flags(requirements)
|
|
2422
|
+
.into_iter()
|
|
2423
|
+
.filter(|flag| match *flag {
|
|
2424
|
+
"--from-spec" => !from_spec,
|
|
2425
|
+
"--confirm" => !confirm,
|
|
2426
|
+
"--force" => !force,
|
|
2427
|
+
_ => false,
|
|
2428
|
+
})
|
|
2429
|
+
.collect()
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
fn remove_agent_required_flags(
|
|
2433
|
+
requirements: &crate::lifecycle::RemoveAgentFlagRequirements,
|
|
2434
|
+
) -> Vec<&'static str> {
|
|
2435
|
+
let mut flags = Vec::new();
|
|
2436
|
+
if requirements.from_spec_required {
|
|
2437
|
+
flags.push("--from-spec");
|
|
2438
|
+
}
|
|
2439
|
+
flags.push("--confirm");
|
|
2440
|
+
if requirements.force_required {
|
|
2441
|
+
flags.push("--force");
|
|
2442
|
+
}
|
|
2443
|
+
flags
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
fn remove_agent_command(
|
|
2447
|
+
workspace: &Path,
|
|
2448
|
+
agent: &str,
|
|
2449
|
+
team: Option<&str>,
|
|
2450
|
+
required_flags: &[&str],
|
|
2451
|
+
) -> String {
|
|
2452
|
+
let mut parts = vec![
|
|
2453
|
+
"team-agent".to_string(),
|
|
2454
|
+
"remove-agent".to_string(),
|
|
2455
|
+
shell_arg(agent),
|
|
2456
|
+
"--workspace".to_string(),
|
|
2457
|
+
shell_arg(&workspace.to_string_lossy()),
|
|
2458
|
+
];
|
|
2459
|
+
if let Some(team) = team {
|
|
2460
|
+
parts.push("--team".to_string());
|
|
2461
|
+
parts.push(shell_arg(team));
|
|
2462
|
+
}
|
|
2463
|
+
parts.extend(required_flags.iter().map(|flag| (*flag).to_string()));
|
|
2464
|
+
parts.join(" ")
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
fn shell_arg(raw: &str) -> String {
|
|
2468
|
+
if !raw.is_empty()
|
|
2469
|
+
&& raw
|
|
2470
|
+
.bytes()
|
|
2471
|
+
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'/' | b'.' | b'_' | b'-' | b':'))
|
|
2472
|
+
{
|
|
2473
|
+
raw.to_string()
|
|
2474
|
+
} else {
|
|
2475
|
+
format!("'{}'", raw.replace('\'', "'\\''"))
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2349
2478
|
/// `runtime.acknowledge_idle`(`cmd_acknowledge_idle`)。
|
|
2350
2479
|
pub fn acknowledge_idle(workspace: &Path, team: Option<&str>) -> Result<Value, CliError> {
|
|
2351
2480
|
let mut state = crate::state::persist::load_runtime_state(workspace)
|
|
@@ -39,7 +39,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
39
39
|
args.json,
|
|
40
40
|
)?;
|
|
41
41
|
add_send_reminder_if_ok(&mut value);
|
|
42
|
-
return Ok(
|
|
42
|
+
return Ok(cmd_send_result(value, args.json));
|
|
43
43
|
}
|
|
44
44
|
// F1 (0.3.26, cross-team send): --pane <pane_id> direct targeting.
|
|
45
45
|
// Mutually exclusive with target / --to (agent-name routing).
|
|
@@ -68,7 +68,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
68
68
|
args.json,
|
|
69
69
|
)?;
|
|
70
70
|
add_send_reminder_if_ok(&mut value);
|
|
71
|
-
return Ok(
|
|
71
|
+
return Ok(cmd_send_result(value, args.json));
|
|
72
72
|
}
|
|
73
73
|
let selected = crate::state::selector::resolve_active_team(
|
|
74
74
|
&args.workspace,
|
|
@@ -88,7 +88,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
88
88
|
if let Some(amb) =
|
|
89
89
|
routing_ambiguous_value(&selected.run_workspace, args, &target, &content, &opts)
|
|
90
90
|
{
|
|
91
|
-
return Ok(
|
|
91
|
+
return Ok(cmd_send_result(amb, args.json));
|
|
92
92
|
}
|
|
93
93
|
let mut outcome = messaging::send_message(&selected.run_workspace, &target, &content, &opts)?;
|
|
94
94
|
if opts.watch_result {
|
|
@@ -101,7 +101,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
add_send_reminder_if_ok(&mut value);
|
|
104
|
-
Ok(
|
|
104
|
+
Ok(cmd_send_result(value, args.json))
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/// F1 (0.3.26): direct pane-id send — bypasses agent-name routing + team
|
|
@@ -752,8 +752,98 @@ fn add_send_reminder_if_ok(value: &mut Value) {
|
|
|
752
752
|
if value.get("ok").and_then(Value::as_bool) != Some(true) {
|
|
753
753
|
return;
|
|
754
754
|
}
|
|
755
|
+
let reminder = send_reminder_for_value(value);
|
|
755
756
|
if let Some(obj) = value.as_object_mut() {
|
|
756
|
-
obj.insert("reminder".to_string(), json!(
|
|
757
|
+
obj.insert("reminder".to_string(), json!(reminder));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
fn cmd_send_result(value: Value, as_json: bool) -> CmdResult {
|
|
762
|
+
let exit = if value.get("ok").and_then(Value::as_bool) == Some(false) {
|
|
763
|
+
ExitCode::Error
|
|
764
|
+
} else {
|
|
765
|
+
ExitCode::Ok
|
|
766
|
+
};
|
|
767
|
+
if as_json {
|
|
768
|
+
CmdResult::from_json(value, true)
|
|
769
|
+
} else {
|
|
770
|
+
CmdResult {
|
|
771
|
+
output: CmdOutput::Human(send_human_output(&value)),
|
|
772
|
+
exit,
|
|
773
|
+
as_json: false,
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
fn send_human_output(value: &Value) -> String {
|
|
779
|
+
let mut parts = vec![
|
|
780
|
+
send_human_field(value, "ok"),
|
|
781
|
+
format!("status: {}", send_human_status(value)),
|
|
782
|
+
send_human_field(value, "message_id"),
|
|
783
|
+
format!("target: {}", send_human_target(value)),
|
|
784
|
+
];
|
|
785
|
+
for key in ["verification", "stage", "reason", "channel"] {
|
|
786
|
+
if !value.get(key).is_none_or(Value::is_null) {
|
|
787
|
+
parts.push(send_human_field(value, key));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
parts.join(" ")
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
fn send_human_field(value: &Value, key: &str) -> String {
|
|
794
|
+
let rendered = value
|
|
795
|
+
.get(key)
|
|
796
|
+
.map(send_human_value)
|
|
797
|
+
.unwrap_or_else(|| "None".to_string());
|
|
798
|
+
format!("{key}: {rendered}")
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
fn send_human_target(value: &Value) -> String {
|
|
802
|
+
["target", "agent_id", "pane_id", "to_name"]
|
|
803
|
+
.iter()
|
|
804
|
+
.find_map(|key| value.get(*key).filter(|v| !v.is_null()))
|
|
805
|
+
.map(send_human_value)
|
|
806
|
+
.unwrap_or_else(|| "None".to_string())
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
fn send_human_status(value: &Value) -> String {
|
|
810
|
+
value
|
|
811
|
+
.get("status")
|
|
812
|
+
.map(send_human_value)
|
|
813
|
+
.unwrap_or_else(|| {
|
|
814
|
+
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
815
|
+
"delivered".to_string()
|
|
816
|
+
} else {
|
|
817
|
+
"failed".to_string()
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
fn send_human_value(value: &Value) -> String {
|
|
823
|
+
let text = match value {
|
|
824
|
+
Value::Null => "None".to_string(),
|
|
825
|
+
Value::Bool(true) => "True".to_string(),
|
|
826
|
+
Value::Bool(false) => "False".to_string(),
|
|
827
|
+
Value::Number(n) => n.to_string(),
|
|
828
|
+
Value::String(s) => s.clone(),
|
|
829
|
+
Value::Array(_) | Value::Object(_) => {
|
|
830
|
+
serde_json::to_string(value).unwrap_or_else(|_| "None".to_string())
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
text.replace(['\r', '\n'], " ")
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
fn send_reminder_for_value(value: &Value) -> &'static str {
|
|
837
|
+
let delivered = value.get("delivered").and_then(Value::as_bool);
|
|
838
|
+
let status = value.get("status").and_then(Value::as_str);
|
|
839
|
+
let delivery_status = value.get("delivery_status").and_then(Value::as_str);
|
|
840
|
+
if delivered == Some(false)
|
|
841
|
+
|| matches!(status, Some("queued"))
|
|
842
|
+
|| matches!(delivery_status, Some("pending"))
|
|
843
|
+
{
|
|
844
|
+
"Message queued; coordinator will notify when the worker receives it. Do not poll the worker terminal with capture-pane."
|
|
845
|
+
} else {
|
|
846
|
+
crate::cli::SEND_REMINDER
|
|
757
847
|
}
|
|
758
848
|
}
|
|
759
849
|
|
|
@@ -493,6 +493,60 @@ fn seed_team_spec(ws: &std::path::Path) {
|
|
|
493
493
|
.unwrap();
|
|
494
494
|
}
|
|
495
495
|
#[test]
|
|
496
|
+
fn remove_agent_spec_running_refusal_lists_all_required_flags_once() {
|
|
497
|
+
let ws = tmp_workspace();
|
|
498
|
+
seed_remove_agent_workspace(&ws, "running");
|
|
499
|
+
let out = json_output(
|
|
500
|
+
cmd_remove_agent(&RemoveAgentArgs {
|
|
501
|
+
agent: "fake_impl".to_string(),
|
|
502
|
+
workspace: ws.clone(),
|
|
503
|
+
team: None,
|
|
504
|
+
from_spec: false,
|
|
505
|
+
confirm: false,
|
|
506
|
+
force: false,
|
|
507
|
+
json: true,
|
|
508
|
+
})
|
|
509
|
+
.unwrap(),
|
|
510
|
+
);
|
|
511
|
+
assert_eq!(out["ok"], json!(false));
|
|
512
|
+
assert_eq!(out["status"], json!("refused"));
|
|
513
|
+
assert_eq!(out["reason"], json!("remove_agent_flags_required"));
|
|
514
|
+
for flag in ["--from-spec", "--confirm", "--force"] {
|
|
515
|
+
assert!(
|
|
516
|
+
out["error"].as_str().is_some_and(|s| s.contains(flag))
|
|
517
|
+
&& out["action"].as_str().is_some_and(|s| s.contains(flag))
|
|
518
|
+
&& out["command"].as_str().is_some_and(|s| s.contains(flag)),
|
|
519
|
+
"refusal must mention {flag} in error/action/copyable command; got {out}"
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
let with_confirm = json_output(
|
|
523
|
+
cmd_remove_agent(&RemoveAgentArgs {
|
|
524
|
+
agent: "fake_impl".to_string(),
|
|
525
|
+
workspace: ws.clone(),
|
|
526
|
+
team: None,
|
|
527
|
+
from_spec: false,
|
|
528
|
+
confirm: true,
|
|
529
|
+
force: false,
|
|
530
|
+
json: true,
|
|
531
|
+
})
|
|
532
|
+
.unwrap(),
|
|
533
|
+
);
|
|
534
|
+
assert_eq!(with_confirm["reason"], json!("remove_agent_flags_required"));
|
|
535
|
+
for flag in ["--from-spec", "--confirm", "--force"] {
|
|
536
|
+
assert!(
|
|
537
|
+
with_confirm["error"].as_str().is_some_and(|s| s.contains(flag))
|
|
538
|
+
&& with_confirm["action"].as_str().is_some_and(|s| s.contains(flag))
|
|
539
|
+
&& with_confirm["command"].as_str().is_some_and(|s| s.contains(flag)),
|
|
540
|
+
"refusal with --confirm must still give the full command including {flag}; got {with_confirm}"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
let state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
544
|
+
assert!(
|
|
545
|
+
state["agents"].get("fake_impl").is_some(),
|
|
546
|
+
"refused remove-agent must not delete the spec-defined running agent"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
#[test]
|
|
496
550
|
fn remove_agent_running_refusal_is_not_success_envelope() {
|
|
497
551
|
let ws = tmp_workspace();
|
|
498
552
|
seed_remove_agent_workspace(&ws, "running");
|
|
@@ -451,6 +451,28 @@ use super::*;
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
fn queued_send_args_fixture(json: bool) -> SendArgs {
|
|
455
|
+
let ws = deleg_uniq_dir("send-human");
|
|
456
|
+
let _ = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
457
|
+
crate::state::persist::save_runtime_state(
|
|
458
|
+
&ws,
|
|
459
|
+
&json!({
|
|
460
|
+
"active_team_key": "current",
|
|
461
|
+
"teams": {"current": {"agents": {"w1": {"provider": "codex"}}}}
|
|
462
|
+
}),
|
|
463
|
+
)
|
|
464
|
+
.unwrap();
|
|
465
|
+
SendArgs {
|
|
466
|
+
workspace: ws,
|
|
467
|
+
target: Some("w1".into()),
|
|
468
|
+
team: None,
|
|
469
|
+
task: None,
|
|
470
|
+
watch_result: false,
|
|
471
|
+
json,
|
|
472
|
+
..send_args_fixture()
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
454
476
|
#[test]
|
|
455
477
|
fn send_options_negates_no_ack_and_no_wait_and_carries_watch() {
|
|
456
478
|
// golden (commands.py:172,174,176): requires_ack=not no_ack; wait_visible=not no_wait;
|
|
@@ -494,7 +516,11 @@ use super::*;
|
|
|
494
516
|
fn cmd_send_joins_message_with_single_space() {
|
|
495
517
|
// golden (commands.py:169): " ".join(["hello","world","foo"]) == "hello world foo".
|
|
496
518
|
// Drive cmd_send; the joined content must reach send_message (RED until ported).
|
|
497
|
-
let
|
|
519
|
+
let args = SendArgs {
|
|
520
|
+
json: true,
|
|
521
|
+
..send_args_fixture()
|
|
522
|
+
};
|
|
523
|
+
let r = cmd_send(&args).expect("cmd_send returns CmdResult");
|
|
498
524
|
// The delegate's DeliveryOutcome -> Json must carry an `ok` key feeding exit-code.
|
|
499
525
|
match r.output {
|
|
500
526
|
CmdOutput::Json(ref v) => {
|
|
@@ -510,11 +536,92 @@ use super::*;
|
|
|
510
536
|
}
|
|
511
537
|
}
|
|
512
538
|
|
|
539
|
+
#[test]
|
|
540
|
+
fn cmd_send_default_human_output_is_one_line_without_false_delivered() {
|
|
541
|
+
let r = cmd_send(&queued_send_args_fixture(false)).expect("cmd_send returns CmdResult");
|
|
542
|
+
assert!(!r.as_json);
|
|
543
|
+
let text = emit(&r.output, r.as_json).expect("send should render human text");
|
|
544
|
+
let lines: Vec<_> = text.lines().collect();
|
|
545
|
+
assert_eq!(lines.len(), 1, "default send output must be one line: {text}");
|
|
546
|
+
assert!(
|
|
547
|
+
lines[0].contains("ok:")
|
|
548
|
+
&& lines[0].contains("status:")
|
|
549
|
+
&& lines[0].contains("message_id:")
|
|
550
|
+
&& lines[0].contains("target:"),
|
|
551
|
+
"default send output must keep only the core fields; got {text}"
|
|
552
|
+
);
|
|
553
|
+
assert!(
|
|
554
|
+
!text.contains("delivered"),
|
|
555
|
+
"queued send output must not claim or mention delivered; got {text}"
|
|
556
|
+
);
|
|
557
|
+
for hidden in [
|
|
558
|
+
"agent_id:",
|
|
559
|
+
"sender:",
|
|
560
|
+
"message_status:",
|
|
561
|
+
"verification:",
|
|
562
|
+
"stage:",
|
|
563
|
+
"reason:",
|
|
564
|
+
"channel:",
|
|
565
|
+
"reminder:",
|
|
566
|
+
] {
|
|
567
|
+
assert!(
|
|
568
|
+
!text.contains(hidden),
|
|
569
|
+
"default send output should hide {hidden} unless needed; got {text}"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#[test]
|
|
575
|
+
fn cmd_send_json_shape_keeps_056_fields() {
|
|
576
|
+
let args = queued_send_args_fixture(true);
|
|
577
|
+
let r = cmd_send(&args).expect("cmd_send returns CmdResult");
|
|
578
|
+
let v = match r.output {
|
|
579
|
+
CmdOutput::Json(v) => v,
|
|
580
|
+
other => panic!("--json send must emit Json, got {other:?}"),
|
|
581
|
+
};
|
|
582
|
+
let obj = v.as_object().expect("--json send output must be object");
|
|
583
|
+
for key in [
|
|
584
|
+
"ok",
|
|
585
|
+
"status",
|
|
586
|
+
"delivery_status",
|
|
587
|
+
"delivered",
|
|
588
|
+
"target",
|
|
589
|
+
"agent_id",
|
|
590
|
+
"content_length_bytes",
|
|
591
|
+
"sender",
|
|
592
|
+
"message_id",
|
|
593
|
+
"message_status",
|
|
594
|
+
"verification",
|
|
595
|
+
"stage",
|
|
596
|
+
"reason",
|
|
597
|
+
"channel",
|
|
598
|
+
"reminder",
|
|
599
|
+
] {
|
|
600
|
+
assert!(obj.contains_key(key), "--json send shape lost {key}: {v}");
|
|
601
|
+
}
|
|
602
|
+
assert_eq!(v.get("verification"), Some(&serde_json::Value::Null));
|
|
603
|
+
assert_eq!(v.get("stage"), Some(&serde_json::Value::Null));
|
|
604
|
+
assert_eq!(v.get("reason"), Some(&serde_json::Value::Null));
|
|
605
|
+
assert_eq!(v.get("channel"), Some(&serde_json::Value::Null));
|
|
606
|
+
assert_eq!(v.get("delivered").and_then(|d| d.as_bool()), Some(false));
|
|
607
|
+
assert!(
|
|
608
|
+
!v.get("reminder")
|
|
609
|
+
.and_then(|reminder| reminder.as_str())
|
|
610
|
+
.unwrap_or_default()
|
|
611
|
+
.contains("Message delivered."),
|
|
612
|
+
"queued JSON reminder must not contradict delivered:false: {v}"
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
513
616
|
#[test]
|
|
514
617
|
fn cmd_send_watch_result_does_not_register_before_delivery() {
|
|
515
618
|
// 0.5.x send contract: --watch-result may only advertise a watcher after
|
|
516
619
|
// initial worker delivery is physically proven.
|
|
517
|
-
let
|
|
620
|
+
let args = SendArgs {
|
|
621
|
+
json: true,
|
|
622
|
+
..send_args_fixture()
|
|
623
|
+
};
|
|
624
|
+
let r = cmd_send(&args).expect("cmd_send returns CmdResult");
|
|
518
625
|
let v = match r.output {
|
|
519
626
|
CmdOutput::Json(v) => v,
|
|
520
627
|
other => panic!("expected Json, got {other:?}"),
|
|
@@ -60,15 +60,37 @@ pub fn remove_agent_with_transport(
|
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
fn
|
|
63
|
+
pub fn remove_agent_flag_requirements(
|
|
64
|
+
workspace: &Path,
|
|
65
|
+
agent_id: &AgentId,
|
|
66
|
+
team: Option<&str>,
|
|
67
|
+
) -> Result<RemoveAgentFlagRequirements, LifecycleError> {
|
|
68
|
+
let paths = lifecycle_paths(workspace, team)?;
|
|
69
|
+
let transport =
|
|
70
|
+
lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
|
|
71
|
+
Ok(remove_agent_preflight(
|
|
72
|
+
&paths.run_workspace,
|
|
73
|
+
&paths.spec_workspace,
|
|
74
|
+
agent_id,
|
|
75
|
+
team,
|
|
76
|
+
&transport,
|
|
77
|
+
)?
|
|
78
|
+
.requirements)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
struct RemoveAgentPreflight {
|
|
82
|
+
state: serde_json::Value,
|
|
83
|
+
spec: YamlValue,
|
|
84
|
+
requirements: RemoveAgentFlagRequirements,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn remove_agent_preflight(
|
|
64
88
|
workspace: &Path,
|
|
65
89
|
spec_workspace: &Path,
|
|
66
90
|
agent_id: &AgentId,
|
|
67
|
-
from_spec: bool,
|
|
68
|
-
force: bool,
|
|
69
91
|
team: Option<&str>,
|
|
70
92
|
transport: &dyn crate::transport::Transport,
|
|
71
|
-
) -> Result<
|
|
93
|
+
) -> Result<RemoveAgentPreflight, LifecycleError> {
|
|
72
94
|
// golden agents.py:34-41: resolve_team_scoped_state FIRST (surfaces the team_target_ambiguous /
|
|
73
95
|
// team_target_unresolved refusal before the owner gate), THEN the owner gate, THEN load_spec +
|
|
74
96
|
// _find_worker (unknown-worker raise). Mirror the stop/reset wiring so remove is byte-symmetric:
|
|
@@ -80,27 +102,84 @@ fn remove_agent_at_paths(
|
|
|
80
102
|
return Err(unknown_worker(agent_id));
|
|
81
103
|
};
|
|
82
104
|
let dynamic_agent = is_dynamic_agent(&state, spec_agent, agent_id);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
let force_required = agent_is_running(&state, agent_id, transport);
|
|
106
|
+
let has_session = state
|
|
107
|
+
.get("session_name")
|
|
108
|
+
.and_then(|v| v.as_str())
|
|
109
|
+
.is_some_and(|s| !s.is_empty())
|
|
110
|
+
|| state
|
|
111
|
+
.get("agents")
|
|
112
|
+
.and_then(|v| v.get(agent_id.as_str()))
|
|
113
|
+
.is_some_and(agent_has_session);
|
|
114
|
+
Ok(RemoveAgentPreflight {
|
|
115
|
+
state,
|
|
116
|
+
spec,
|
|
117
|
+
requirements: RemoveAgentFlagRequirements {
|
|
90
118
|
agent_id: agent_id.clone(),
|
|
119
|
+
from_spec_required: !dynamic_agent,
|
|
120
|
+
force_required,
|
|
121
|
+
has_session,
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn agent_has_session(agent: &serde_json::Value) -> bool {
|
|
127
|
+
["session_id", "_pending_session_id", "rollout_path"]
|
|
128
|
+
.iter()
|
|
129
|
+
.any(|key| {
|
|
130
|
+
agent
|
|
131
|
+
.get(key)
|
|
132
|
+
.and_then(|v| v.as_str())
|
|
133
|
+
.is_some_and(|s| !s.is_empty())
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn remove_agent_at_paths(
|
|
138
|
+
workspace: &Path,
|
|
139
|
+
spec_workspace: &Path,
|
|
140
|
+
agent_id: &AgentId,
|
|
141
|
+
from_spec: bool,
|
|
142
|
+
force: bool,
|
|
143
|
+
team: Option<&str>,
|
|
144
|
+
transport: &dyn crate::transport::Transport,
|
|
145
|
+
) -> Result<RemoveAgentOutcome, LifecycleError> {
|
|
146
|
+
let preflight = remove_agent_preflight(workspace, spec_workspace, agent_id, team, transport)?;
|
|
147
|
+
let missing_from_spec = preflight.requirements.from_spec_required && !from_spec;
|
|
148
|
+
let missing_force = preflight.requirements.force_required && !force;
|
|
149
|
+
if missing_from_spec || missing_force {
|
|
150
|
+
return Ok(if missing_from_spec && missing_force {
|
|
151
|
+
RemoveAgentOutcome::RefusedRequiredFlags {
|
|
152
|
+
agent_id: agent_id.clone(),
|
|
153
|
+
from_spec_required: true,
|
|
154
|
+
force_required: true,
|
|
155
|
+
}
|
|
156
|
+
} else if missing_from_spec {
|
|
157
|
+
RemoveAgentOutcome::RefusedFromSpecConfirm {
|
|
158
|
+
agent_id: agent_id.clone(),
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
RemoveAgentOutcome::RefusedForceRequired {
|
|
162
|
+
agent_id: agent_id.clone(),
|
|
163
|
+
}
|
|
91
164
|
});
|
|
92
165
|
}
|
|
93
166
|
let paths = LifecyclePathRefs {
|
|
94
167
|
run_workspace: workspace,
|
|
95
168
|
spec_workspace,
|
|
96
169
|
};
|
|
97
|
-
let mut rollback = RemoveRollback::capture(
|
|
98
|
-
|
|
170
|
+
let mut rollback = RemoveRollback::capture(
|
|
171
|
+
paths.run_workspace,
|
|
172
|
+
paths.spec_workspace,
|
|
173
|
+
&preflight.spec,
|
|
174
|
+
&preflight.state,
|
|
175
|
+
agent_id,
|
|
176
|
+
)?;
|
|
177
|
+
rollback.restore_running = force && preflight.requirements.force_required;
|
|
99
178
|
let result = remove_agent_inner(
|
|
100
179
|
&paths,
|
|
101
180
|
agent_id,
|
|
102
|
-
&spec,
|
|
103
|
-
state,
|
|
181
|
+
&preflight.spec,
|
|
182
|
+
preflight.state,
|
|
104
183
|
force,
|
|
105
184
|
team,
|
|
106
185
|
transport,
|
|
@@ -50,7 +50,7 @@ pub use rebuild::{
|
|
|
50
50
|
restart, restart_candidates, restart_with_session_convergence_deadline, restart_with_transport,
|
|
51
51
|
restart_with_transport_with_readiness_deadline, select_restart_state,
|
|
52
52
|
};
|
|
53
|
-
pub use remove::{remove_agent, remove_agent_with_transport};
|
|
53
|
+
pub use remove::{remove_agent, remove_agent_flag_requirements, remove_agent_with_transport};
|
|
54
54
|
pub use selection::{
|
|
55
55
|
classify_first_send_at, classify_restart_plan, decide_start_mode, python_type_name,
|
|
56
56
|
};
|
|
@@ -609,6 +609,15 @@ pub struct ForkAgentReport {
|
|
|
609
609
|
pub session_id: Option<SessionId>,
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
+
/// Read-only remove-agent flag requirements for the target's current state.
|
|
613
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
614
|
+
pub struct RemoveAgentFlagRequirements {
|
|
615
|
+
pub agent_id: AgentId,
|
|
616
|
+
pub from_spec_required: bool,
|
|
617
|
+
pub force_required: bool,
|
|
618
|
+
pub has_session: bool,
|
|
619
|
+
}
|
|
620
|
+
|
|
612
621
|
/// `remove_agent(...)` 结果(`agents.py:54/56/150`)。`_RemoveRollback` 快照
|
|
613
622
|
/// spec/state/team_state/role_file/agent_health 字节级回滚(Gap 16)。
|
|
614
623
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -624,6 +633,12 @@ pub enum RemoveAgentOutcome {
|
|
|
624
633
|
RefusedFromSpecConfirm { agent_id: AgentId },
|
|
625
634
|
/// 运行中未传 force(`agents.py:56`)。
|
|
626
635
|
RefusedForceRequired { agent_id: AgentId },
|
|
636
|
+
/// Multiple required destructive flags are absent; report them together.
|
|
637
|
+
RefusedRequiredFlags {
|
|
638
|
+
agent_id: AgentId,
|
|
639
|
+
from_spec_required: bool,
|
|
640
|
+
force_required: bool,
|
|
641
|
+
},
|
|
627
642
|
}
|
|
628
643
|
|
|
629
644
|
/// `restart(...)` 结果(`orchestration.py:114/142/387`)。Route B:**先全量验证**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.7",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.7",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.7"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|