@team-agent/installer 0.3.34 → 0.3.36
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 +33 -11
- package/crates/team-agent/src/cli/mod.rs +43 -7
- package/crates/team-agent/src/cli/send.rs +12 -1
- package/crates/team-agent/src/cli/status_port.rs +7 -1
- package/crates/team-agent/src/cli/tests/status_send.rs +85 -0
- package/crates/team-agent/src/leader/start.rs +129 -5
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -152,7 +152,7 @@ fn quickstart_human(value: &Value) -> String {
|
|
|
152
152
|
.map(|items| items.iter().filter_map(Value::as_str).collect())
|
|
153
153
|
.unwrap_or_default();
|
|
154
154
|
if attach.is_empty() {
|
|
155
|
-
return summary.to_string();
|
|
155
|
+
return append_reminder(summary.to_string(), crate::cli::QUICK_START_REMINDER);
|
|
156
156
|
}
|
|
157
157
|
let mut out = String::from(summary);
|
|
158
158
|
out.push_str("\n\nattach:");
|
|
@@ -160,7 +160,15 @@ fn quickstart_human(value: &Value) -> String {
|
|
|
160
160
|
out.push_str("\n ");
|
|
161
161
|
out.push_str(cmd);
|
|
162
162
|
}
|
|
163
|
-
out
|
|
163
|
+
append_reminder(out, crate::cli::QUICK_START_REMINDER)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn append_reminder(text: String, reminder: &str) -> String {
|
|
167
|
+
if text.is_empty() {
|
|
168
|
+
reminder.to_string()
|
|
169
|
+
} else {
|
|
170
|
+
format!("{text}\n{reminder}")
|
|
171
|
+
}
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
/// `cmd_compile`(`commands.py:42`)。
|
|
@@ -240,7 +248,10 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
240
248
|
true,
|
|
241
249
|
false,
|
|
242
250
|
)?;
|
|
243
|
-
return Ok(CmdResult::human(
|
|
251
|
+
return Ok(CmdResult::human(append_reminder(
|
|
252
|
+
format_status_summary(&value),
|
|
253
|
+
crate::cli::STATUS_REMINDER,
|
|
254
|
+
)));
|
|
244
255
|
}
|
|
245
256
|
if args.json {
|
|
246
257
|
let value = status_port::status_scoped(
|
|
@@ -252,12 +263,15 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
252
263
|
)?;
|
|
253
264
|
return Ok(CmdResult::from_json(value, true));
|
|
254
265
|
}
|
|
255
|
-
Ok(CmdResult::human(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
Ok(CmdResult::human(append_reminder(
|
|
267
|
+
status_port::format_status_scoped(
|
|
268
|
+
&selected.run_workspace,
|
|
269
|
+
&selected.state,
|
|
270
|
+
Some(&selected.team_key),
|
|
271
|
+
args.agent.as_deref(),
|
|
272
|
+
)?,
|
|
273
|
+
crate::cli::STATUS_REMINDER,
|
|
274
|
+
)))
|
|
261
275
|
}
|
|
262
276
|
|
|
263
277
|
fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
|
|
@@ -276,6 +290,7 @@ fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
|
|
|
276
290
|
"action": "run `team-agent doctor` or inspect the log path shown here",
|
|
277
291
|
"log": log_path.to_string_lossy().to_string(),
|
|
278
292
|
"workspace": workspace.to_string_lossy().to_string(),
|
|
293
|
+
"reminder": crate::cli::STATUS_REMINDER,
|
|
279
294
|
})
|
|
280
295
|
}
|
|
281
296
|
|
|
@@ -1583,15 +1598,22 @@ mod tests {
|
|
|
1583
1598
|
assert!(out.contains("team started"), "must keep summary; got {out}");
|
|
1584
1599
|
assert!(out.contains("attach:"), "must render attach block; got {out}");
|
|
1585
1600
|
assert!(out.contains("team-y:w1") && out.contains("team-y:w2"), "must list each attach cmd; got {out}");
|
|
1601
|
+
assert!(out.ends_with(crate::cli::QUICK_START_REMINDER), "must append harness reminder; got {out}");
|
|
1586
1602
|
}
|
|
1587
1603
|
|
|
1588
1604
|
#[test]
|
|
1589
1605
|
fn e13_quickstart_human_summary_only_when_no_attach() {
|
|
1590
1606
|
let value = json!({"summary": "quick-start complete"});
|
|
1591
|
-
assert_eq!(
|
|
1607
|
+
assert_eq!(
|
|
1608
|
+
quickstart_human(&value),
|
|
1609
|
+
format!("quick-start complete\n{}", crate::cli::QUICK_START_REMINDER)
|
|
1610
|
+
);
|
|
1592
1611
|
// 空数组也只 summary。
|
|
1593
1612
|
let value2 = json!({"summary": "s", "attach_commands": []});
|
|
1594
|
-
assert_eq!(
|
|
1613
|
+
assert_eq!(
|
|
1614
|
+
quickstart_human(&value2),
|
|
1615
|
+
format!("s\n{}", crate::cli::QUICK_START_REMINDER)
|
|
1616
|
+
);
|
|
1595
1617
|
}
|
|
1596
1618
|
|
|
1597
1619
|
#[test]
|
|
@@ -48,6 +48,9 @@ use crate::messaging::{self, AlertType, MessageTarget, SendOptions};
|
|
|
48
48
|
use crate::model::ids::{TaskId, TeamKey};
|
|
49
49
|
|
|
50
50
|
pub(crate) const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
51
|
+
pub(crate) const QUICK_START_REMINDER: &str = "Reminder: Do not inspect raw worker terminal output during normal operation. Use team-agent status / inbox / collect instead. Wait for report_result.";
|
|
52
|
+
pub(crate) const SEND_REMINDER: &str = "Message delivered. Wait for the worker to report_result. Do not poll the worker terminal with capture-pane.";
|
|
53
|
+
pub(crate) const STATUS_REMINDER: &str = "To wait for results use --watch-result or team-agent collect. Do not capture-pane worker terminals.";
|
|
51
54
|
|
|
52
55
|
pub mod adapters;
|
|
53
56
|
pub mod diagnose;
|
|
@@ -158,13 +161,7 @@ pub mod lifecycle_port {
|
|
|
158
161
|
crate::leader::LeaderLaunchStatus::Detached => true,
|
|
159
162
|
crate::leader::LeaderLaunchStatus::NotStarted => false,
|
|
160
163
|
};
|
|
161
|
-
let leader_attach_command =
|
|
162
|
-
None
|
|
163
|
-
} else {
|
|
164
|
-
plan.session_name.as_ref().and_then(|session| {
|
|
165
|
-
crate::tmux_backend::attach_command_for_workspace(cwd, session, "leader")
|
|
166
|
-
})
|
|
167
|
-
};
|
|
164
|
+
let leader_attach_command = leader_attach_command_for_plan(cwd, &plan);
|
|
168
165
|
Ok(json!({
|
|
169
166
|
"ok": ok,
|
|
170
167
|
"provider": provider,
|
|
@@ -182,6 +179,18 @@ pub mod lifecycle_port {
|
|
|
182
179
|
"session_name": plan.session_name.as_ref().map(|session| session.as_str().to_string()),
|
|
183
180
|
}))
|
|
184
181
|
}
|
|
182
|
+
|
|
183
|
+
pub(crate) fn leader_attach_command_for_plan(
|
|
184
|
+
cwd: &Path,
|
|
185
|
+
plan: &crate::leader::LeaderStartPlan,
|
|
186
|
+
) -> Option<String> {
|
|
187
|
+
if plan.is_external_leader {
|
|
188
|
+
return None;
|
|
189
|
+
}
|
|
190
|
+
let session = plan.session_name.as_ref()?;
|
|
191
|
+
let window = plan.leader_window.as_ref()?;
|
|
192
|
+
crate::tmux_backend::attach_command_for_workspace(cwd, session, window.as_str())
|
|
193
|
+
}
|
|
185
194
|
/// `runtime.shutdown`(`cmd_shutdown`)。
|
|
186
195
|
pub fn shutdown(workspace: &Path, keep_logs: bool, team: Option<&str>) -> Result<Value, CliError> {
|
|
187
196
|
let run_ws = crate::model::paths::canonical_run_workspace(workspace)
|
|
@@ -2522,6 +2531,7 @@ pub mod lifecycle_port {
|
|
|
2522
2531
|
"display_backend": display_backend,
|
|
2523
2532
|
"next_actions": next_actions,
|
|
2524
2533
|
"attach_commands": attach_commands,
|
|
2534
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2525
2535
|
"readiness": readiness_json.clone(),
|
|
2526
2536
|
"worker_readiness": readiness_json,
|
|
2527
2537
|
})
|
|
@@ -2540,6 +2550,7 @@ pub mod lifecycle_port {
|
|
|
2540
2550
|
"state_path": state_path.map(|p| p.to_string_lossy().to_string()),
|
|
2541
2551
|
"next_actions": next_actions,
|
|
2542
2552
|
"attach_commands": attach_commands,
|
|
2553
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2543
2554
|
}),
|
|
2544
2555
|
crate::lifecycle::QuickStartReport::PreflightBlocked {
|
|
2545
2556
|
summary,
|
|
@@ -2552,6 +2563,7 @@ pub mod lifecycle_port {
|
|
|
2552
2563
|
"blockers": blockers,
|
|
2553
2564
|
"next_actions": next_actions,
|
|
2554
2565
|
"attach_commands": attach_commands,
|
|
2566
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2555
2567
|
}),
|
|
2556
2568
|
}
|
|
2557
2569
|
}
|
|
@@ -2576,6 +2588,10 @@ pub mod lifecycle_port {
|
|
|
2576
2588
|
Some("tmux -S /tmp/tmux-501/ta-test attach -t team-teamA:worker"),
|
|
2577
2589
|
"B-2: ExistingRuntime JSON must preserve attach_commands instead of only next_actions; value={value}"
|
|
2578
2590
|
);
|
|
2591
|
+
assert_eq!(
|
|
2592
|
+
value.get("reminder").and_then(Value::as_str),
|
|
2593
|
+
Some(crate::cli::QUICK_START_REMINDER)
|
|
2594
|
+
);
|
|
2579
2595
|
}
|
|
2580
2596
|
|
|
2581
2597
|
#[test]
|
|
@@ -2592,6 +2608,20 @@ pub mod lifecycle_port {
|
|
|
2592
2608
|
"B-2: PreflightBlocked JSON must include attach_commands: [] for schema parity with Ready/Restart; value={value}"
|
|
2593
2609
|
);
|
|
2594
2610
|
}
|
|
2611
|
+
|
|
2612
|
+
#[test]
|
|
2613
|
+
fn restart_json_includes_harness_reminder() {
|
|
2614
|
+
let value = restart_value(crate::lifecycle::RestartReport::RefusedResumeAtomicity {
|
|
2615
|
+
unresumable: Vec::new(),
|
|
2616
|
+
allow_fresh: false,
|
|
2617
|
+
error: "resume refused".to_string(),
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
assert_eq!(
|
|
2621
|
+
value.get("reminder").and_then(Value::as_str),
|
|
2622
|
+
Some(crate::cli::QUICK_START_REMINDER)
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2595
2625
|
}
|
|
2596
2626
|
|
|
2597
2627
|
fn restart_value(report: crate::lifecycle::RestartReport) -> Value {
|
|
@@ -2610,6 +2640,7 @@ pub mod lifecycle_port {
|
|
|
2610
2640
|
"coordinator_started": coordinator_started,
|
|
2611
2641
|
"next_actions": next_actions,
|
|
2612
2642
|
"attach_commands": attach_commands,
|
|
2643
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2613
2644
|
}),
|
|
2614
2645
|
crate::lifecycle::RestartReport::Partial {
|
|
2615
2646
|
session_name,
|
|
@@ -2644,6 +2675,7 @@ pub mod lifecycle_port {
|
|
|
2644
2675
|
"coordinator_started": coordinator_started,
|
|
2645
2676
|
"next_actions": next_actions,
|
|
2646
2677
|
"attach_commands": attach_commands,
|
|
2678
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2647
2679
|
}),
|
|
2648
2680
|
crate::lifecycle::RestartReport::Failed {
|
|
2649
2681
|
session_name,
|
|
@@ -2675,6 +2707,7 @@ pub mod lifecycle_port {
|
|
|
2675
2707
|
})).collect::<Vec<_>>(),
|
|
2676
2708
|
"next_actions": next_actions,
|
|
2677
2709
|
"attach_commands": attach_commands,
|
|
2710
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2678
2711
|
}),
|
|
2679
2712
|
crate::lifecycle::RestartReport::RefusedResumeAtomicity {
|
|
2680
2713
|
unresumable,
|
|
@@ -2686,6 +2719,7 @@ pub mod lifecycle_port {
|
|
|
2686
2719
|
"allow_fresh": allow_fresh,
|
|
2687
2720
|
"error": error,
|
|
2688
2721
|
"unresumable": unresumable.iter().map(|w| w.agent_id.as_str()).collect::<Vec<_>>(),
|
|
2722
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2689
2723
|
}),
|
|
2690
2724
|
crate::lifecycle::RestartReport::RefusedResumeNotReady {
|
|
2691
2725
|
missing,
|
|
@@ -2710,6 +2744,7 @@ pub mod lifecycle_port {
|
|
|
2710
2744
|
"pending_agent_ids": missing.iter().map(|w| w.as_str()).collect::<Vec<_>>(),
|
|
2711
2745
|
},
|
|
2712
2746
|
"next_action": "rerun restart after session capture completes, or pass --allow-fresh to deliberately discard missing context",
|
|
2747
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2713
2748
|
}),
|
|
2714
2749
|
crate::lifecycle::RestartReport::RefusedInvalidFirstSendAt {
|
|
2715
2750
|
invalid,
|
|
@@ -2721,6 +2756,7 @@ pub mod lifecycle_port {
|
|
|
2721
2756
|
"allow_fresh": allow_fresh,
|
|
2722
2757
|
"error": error,
|
|
2723
2758
|
"invalid": invalid.iter().map(|w| w.worker_id.as_str()).collect::<Vec<_>>(),
|
|
2759
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
2724
2760
|
}),
|
|
2725
2761
|
}
|
|
2726
2762
|
}
|
|
@@ -22,7 +22,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
22
22
|
if content.is_empty() {
|
|
23
23
|
return Err(CliError::Usage("--pane requires a non-empty message".to_string()));
|
|
24
24
|
}
|
|
25
|
-
let value = send_to_pane_direct(
|
|
25
|
+
let mut value = send_to_pane_direct(
|
|
26
26
|
&args.workspace,
|
|
27
27
|
pane_id,
|
|
28
28
|
&content,
|
|
@@ -31,6 +31,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
31
31
|
args.team.as_deref(),
|
|
32
32
|
args.json,
|
|
33
33
|
)?;
|
|
34
|
+
add_send_reminder_if_ok(&mut value);
|
|
34
35
|
return Ok(CmdResult::from_json(value, args.json));
|
|
35
36
|
}
|
|
36
37
|
let selected = crate::state::selector::resolve_active_team(
|
|
@@ -60,6 +61,7 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
60
61
|
obj.insert("watch".to_string(), watch_notice_json(&target, &opts));
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
add_send_reminder_if_ok(&mut value);
|
|
63
65
|
Ok(CmdResult::from_json(value, args.json))
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -471,6 +473,15 @@ fn delivery_outcome_json(
|
|
|
471
473
|
})
|
|
472
474
|
}
|
|
473
475
|
|
|
476
|
+
fn add_send_reminder_if_ok(value: &mut Value) {
|
|
477
|
+
if value.get("ok").and_then(Value::as_bool) != Some(true) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if let Some(obj) = value.as_object_mut() {
|
|
481
|
+
obj.insert("reminder".to_string(), json!(crate::cli::SEND_REMINDER));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
474
485
|
fn target_json(target: &MessageTarget) -> Value {
|
|
475
486
|
match target {
|
|
476
487
|
MessageTarget::Single(agent) => json!(agent),
|
|
@@ -53,11 +53,15 @@ use rusqlite::params;
|
|
|
53
53
|
let leader_attach_command = if is_external_leader {
|
|
54
54
|
None
|
|
55
55
|
} else {
|
|
56
|
+
let window_name = state
|
|
57
|
+
.pointer("/leader_receiver/window_name")
|
|
58
|
+
.and_then(Value::as_str)
|
|
59
|
+
.unwrap_or("leader");
|
|
56
60
|
session_name.as_str().and_then(|session| {
|
|
57
61
|
crate::tmux_backend::attach_command_for_workspace(
|
|
58
62
|
workspace,
|
|
59
63
|
&crate::transport::SessionName::new(session.to_string()),
|
|
60
|
-
|
|
64
|
+
window_name,
|
|
61
65
|
)
|
|
62
66
|
})
|
|
63
67
|
};
|
|
@@ -95,6 +99,7 @@ use rusqlite::params;
|
|
|
95
99
|
"readiness": readiness,
|
|
96
100
|
"coordinator": coordinator_health_value(health),
|
|
97
101
|
"runtime": runtime_block,
|
|
102
|
+
"reminder": crate::cli::STATUS_REMINDER,
|
|
98
103
|
"last_events": Value::Array(
|
|
99
104
|
crate::event_log::EventLog::new(workspace)
|
|
100
105
|
.tail(10)
|
|
@@ -594,6 +599,7 @@ use rusqlite::params;
|
|
|
594
599
|
"latest_results": take_array(full.get("latest_results"), 5),
|
|
595
600
|
"readiness": full.get("readiness").cloned().unwrap_or_else(|| json!({})),
|
|
596
601
|
"coordinator": compact_object(full.get("coordinator"), &["status", "pid", "metadata_ok", "schema_ok"]),
|
|
602
|
+
"reminder": full.get("reminder").cloned().unwrap_or_else(|| json!(crate::cli::STATUS_REMINDER)),
|
|
597
603
|
"last_events": take_array_tail(full.get("last_events"), 10),
|
|
598
604
|
})
|
|
599
605
|
}
|
|
@@ -50,10 +50,15 @@ use super::*;
|
|
|
50
50
|
"results",
|
|
51
51
|
"latest_results",
|
|
52
52
|
"coordinator",
|
|
53
|
+
"reminder",
|
|
53
54
|
"last_events",
|
|
54
55
|
] {
|
|
55
56
|
assert!(obj.contains_key(key), "compact status missing key `{key}`");
|
|
56
57
|
}
|
|
58
|
+
assert_eq!(
|
|
59
|
+
obj.get("reminder").and_then(|v| v.as_str()),
|
|
60
|
+
Some(crate::cli::STATUS_REMINDER)
|
|
61
|
+
);
|
|
57
62
|
// seeded agent surfaces through the projection.
|
|
58
63
|
assert!(
|
|
59
64
|
obj["agents"].as_object().unwrap().contains_key("a1"),
|
|
@@ -65,6 +70,27 @@ use super::*;
|
|
|
65
70
|
let _ = std::fs::remove_dir_all(&ws);
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
#[test]
|
|
74
|
+
fn cmd_status_human_appends_harness_reminder() {
|
|
75
|
+
let ws = seed_status_workspace();
|
|
76
|
+
let args = StatusArgs {
|
|
77
|
+
agent: None,
|
|
78
|
+
workspace: ws.clone(),
|
|
79
|
+
detail: false,
|
|
80
|
+
summary: false,
|
|
81
|
+
json: false,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
let r = cmd_status_for_team(&args, None).expect("status");
|
|
85
|
+
let text = match r.output {
|
|
86
|
+
CmdOutput::Human(text) => text,
|
|
87
|
+
other => panic!("expected human status output, got {other:?}"),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
assert!(text.ends_with(crate::cli::STATUS_REMINDER), "{text}");
|
|
91
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
#[test]
|
|
69
95
|
fn status_port_status_reports_managed_leader_topology_and_attach_command() {
|
|
70
96
|
let ws = seed_status_workspace();
|
|
@@ -86,6 +112,59 @@ use super::*;
|
|
|
86
112
|
let _ = std::fs::remove_dir_all(&ws);
|
|
87
113
|
}
|
|
88
114
|
|
|
115
|
+
#[test]
|
|
116
|
+
fn status_port_managed_attach_command_uses_receiver_window_name() {
|
|
117
|
+
let ws = seed_status_workspace();
|
|
118
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
119
|
+
if let Some(obj) = state.as_object_mut() {
|
|
120
|
+
obj.insert("is_external_leader".to_string(), json!(false));
|
|
121
|
+
obj.insert("session_name".to_string(), json!("team-current"));
|
|
122
|
+
obj.insert(
|
|
123
|
+
"leader_receiver".to_string(),
|
|
124
|
+
json!({"pane_id": "%3", "window_name": "claude_code", "status": "attached"}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
128
|
+
|
|
129
|
+
let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false).expect("status");
|
|
130
|
+
|
|
131
|
+
let attach = v["leader_attach_command"]
|
|
132
|
+
.as_str()
|
|
133
|
+
.expect("managed status includes attach command");
|
|
134
|
+
assert!(attach.contains("attach -t team-current:claude_code"), "{attach}");
|
|
135
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[test]
|
|
139
|
+
fn leader_attach_command_for_plan_uses_plan_leader_window() {
|
|
140
|
+
let ws = tmp_workspace();
|
|
141
|
+
let plan = crate::leader::LeaderStartPlan {
|
|
142
|
+
mode: crate::leader::LeaderStartMode::ManagedTmuxClient,
|
|
143
|
+
provider: crate::provider::Provider::ClaudeCode,
|
|
144
|
+
workspace: ws.clone(),
|
|
145
|
+
socket: crate::leader::LeaderLaunchSocket::Workspace,
|
|
146
|
+
session_name: Some(crate::transport::SessionName::new(
|
|
147
|
+
"team-agent-leader-claude_code-demo".to_string(),
|
|
148
|
+
)),
|
|
149
|
+
argv: Vec::new(),
|
|
150
|
+
provider_argv: Vec::new(),
|
|
151
|
+
leader_window: Some(crate::transport::WindowName::new("claude_code")),
|
|
152
|
+
is_external_leader: false,
|
|
153
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
154
|
+
identity: None,
|
|
155
|
+
detached: false,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
let attach = lifecycle_port::leader_attach_command_for_plan(&ws, &plan)
|
|
159
|
+
.expect("managed plan has attach command");
|
|
160
|
+
|
|
161
|
+
assert!(
|
|
162
|
+
attach.contains("attach -t team-agent-leader-claude_code-demo:claude_code"),
|
|
163
|
+
"{attach}"
|
|
164
|
+
);
|
|
165
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
166
|
+
}
|
|
167
|
+
|
|
89
168
|
#[test]
|
|
90
169
|
fn status_port_missing_topology_marker_defaults_to_managed() {
|
|
91
170
|
let ws = seed_status_workspace();
|
|
@@ -202,6 +281,12 @@ use super::*;
|
|
|
202
281
|
match r.output {
|
|
203
282
|
CmdOutput::Json(ref v) => {
|
|
204
283
|
assert!(v.get("ok").is_some(), "send result Json must carry `ok`");
|
|
284
|
+
if v.get("ok").and_then(|ok| ok.as_bool()) == Some(true) {
|
|
285
|
+
assert_eq!(
|
|
286
|
+
v.get("reminder").and_then(|reminder| reminder.as_str()),
|
|
287
|
+
Some(crate::cli::SEND_REMINDER)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
205
290
|
}
|
|
206
291
|
other => panic!("cmd_send must emit Json DeliveryOutcome, got {other:?}"),
|
|
207
292
|
}
|
|
@@ -7,7 +7,9 @@ use std::process::{Command, Stdio};
|
|
|
7
7
|
|
|
8
8
|
use crate::provider::{get_adapter, Provider};
|
|
9
9
|
use crate::tmux_backend::TmuxBackend;
|
|
10
|
-
use crate::transport::{
|
|
10
|
+
use crate::transport::{
|
|
11
|
+
PaneId, PaneLiveness, SessionName, SpawnResult, Target, Transport, WindowName,
|
|
12
|
+
};
|
|
11
13
|
|
|
12
14
|
use super::helpers::{
|
|
13
15
|
provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
|
|
@@ -405,6 +407,7 @@ fn execute_managed_leader_plan(
|
|
|
405
407
|
.unwrap_or_else(|| "signal".to_string())
|
|
406
408
|
)));
|
|
407
409
|
}
|
|
410
|
+
ensure_managed_provider_live_after_attach(&transport, &spawned)?;
|
|
408
411
|
Ok(LeaderLaunchOutcome {
|
|
409
412
|
status: LeaderLaunchStatus::Exited,
|
|
410
413
|
exit_code: code,
|
|
@@ -414,7 +417,7 @@ fn execute_managed_leader_plan(
|
|
|
414
417
|
}
|
|
415
418
|
|
|
416
419
|
fn ensure_managed_leader_pane(
|
|
417
|
-
transport: &
|
|
420
|
+
transport: &dyn Transport,
|
|
418
421
|
session: &SessionName,
|
|
419
422
|
window: &WindowName,
|
|
420
423
|
plan: &LeaderStartPlan,
|
|
@@ -455,6 +458,39 @@ fn ensure_managed_leader_pane(
|
|
|
455
458
|
}
|
|
456
459
|
}
|
|
457
460
|
|
|
461
|
+
fn ensure_managed_provider_live_after_attach(
|
|
462
|
+
transport: &dyn Transport,
|
|
463
|
+
spawned: &SpawnResult,
|
|
464
|
+
) -> Result<(), LeaderError> {
|
|
465
|
+
let live = match transport.liveness(&spawned.pane_id) {
|
|
466
|
+
Ok(PaneLiveness::Live) => true,
|
|
467
|
+
Ok(PaneLiveness::Dead) => false,
|
|
468
|
+
Ok(PaneLiveness::Unknown) | Err(_) => managed_spawned_pane_in_targets(transport, spawned),
|
|
469
|
+
};
|
|
470
|
+
if live {
|
|
471
|
+
return Ok(());
|
|
472
|
+
}
|
|
473
|
+
Err(LeaderError::Start(format!(
|
|
474
|
+
"managed leader provider pane is not running after tmux client returned: {} {}:{}",
|
|
475
|
+
spawned.pane_id.as_str(),
|
|
476
|
+
spawned.session.as_str(),
|
|
477
|
+
spawned.window.as_str()
|
|
478
|
+
)))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn managed_spawned_pane_in_targets(transport: &dyn Transport, spawned: &SpawnResult) -> bool {
|
|
482
|
+
transport
|
|
483
|
+
.list_targets()
|
|
484
|
+
.unwrap_or_default()
|
|
485
|
+
.iter()
|
|
486
|
+
.any(|pane| {
|
|
487
|
+
pane.pane_id.as_str() == spawned.pane_id.as_str()
|
|
488
|
+
&& pane.session.as_str() == spawned.session.as_str()
|
|
489
|
+
&& pane.window_name.as_ref().map(WindowName::as_str)
|
|
490
|
+
== Some(spawned.window.as_str())
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
458
494
|
fn persist_managed_leader_binding(
|
|
459
495
|
plan: &LeaderStartPlan,
|
|
460
496
|
workspace: &Path,
|
|
@@ -911,11 +947,16 @@ mod tests {
|
|
|
911
947
|
TurnVerification, WindowName,
|
|
912
948
|
};
|
|
913
949
|
|
|
914
|
-
use super::{
|
|
950
|
+
use super::{
|
|
951
|
+
ensure_managed_provider_live_after_attach, execute_leader_plan,
|
|
952
|
+
handle_exec_provider_startup_prompts, shlex_quote,
|
|
953
|
+
};
|
|
915
954
|
|
|
916
955
|
struct ScriptedTransport {
|
|
917
956
|
screens: Mutex<Vec<String>>,
|
|
918
957
|
sent: Mutex<Vec<(Target, Vec<Key>)>>,
|
|
958
|
+
liveness: PaneLiveness,
|
|
959
|
+
targets: Vec<PaneInfo>,
|
|
919
960
|
}
|
|
920
961
|
|
|
921
962
|
impl ScriptedTransport {
|
|
@@ -923,6 +964,26 @@ mod tests {
|
|
|
923
964
|
Self {
|
|
924
965
|
screens: Mutex::new(screens),
|
|
925
966
|
sent: Mutex::new(Vec::new()),
|
|
967
|
+
liveness: PaneLiveness::Unknown,
|
|
968
|
+
targets: Vec::new(),
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
fn with_liveness(liveness: PaneLiveness) -> Self {
|
|
973
|
+
Self {
|
|
974
|
+
screens: Mutex::new(Vec::new()),
|
|
975
|
+
sent: Mutex::new(Vec::new()),
|
|
976
|
+
liveness,
|
|
977
|
+
targets: Vec::new(),
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
fn with_liveness_and_targets(liveness: PaneLiveness, targets: Vec<PaneInfo>) -> Self {
|
|
982
|
+
Self {
|
|
983
|
+
screens: Mutex::new(Vec::new()),
|
|
984
|
+
sent: Mutex::new(Vec::new()),
|
|
985
|
+
liveness,
|
|
986
|
+
targets,
|
|
926
987
|
}
|
|
927
988
|
}
|
|
928
989
|
|
|
@@ -1024,11 +1085,11 @@ mod tests {
|
|
|
1024
1085
|
}
|
|
1025
1086
|
|
|
1026
1087
|
fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
1027
|
-
Ok(
|
|
1088
|
+
Ok(self.liveness)
|
|
1028
1089
|
}
|
|
1029
1090
|
|
|
1030
1091
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
1031
|
-
Ok(
|
|
1092
|
+
Ok(self.targets.clone())
|
|
1032
1093
|
}
|
|
1033
1094
|
|
|
1034
1095
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
@@ -1063,6 +1124,69 @@ mod tests {
|
|
|
1063
1124
|
}
|
|
1064
1125
|
}
|
|
1065
1126
|
|
|
1127
|
+
fn managed_spawn_result() -> SpawnResult {
|
|
1128
|
+
SpawnResult {
|
|
1129
|
+
pane_id: PaneId::new("%42"),
|
|
1130
|
+
session: SessionName::new("team-agent-leader-claude_code-demo"),
|
|
1131
|
+
window: WindowName::new("claude_code"),
|
|
1132
|
+
child_pid: Some(1234),
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
fn managed_pane_info(spawned: &SpawnResult) -> PaneInfo {
|
|
1137
|
+
PaneInfo {
|
|
1138
|
+
pane_id: spawned.pane_id.clone(),
|
|
1139
|
+
session: spawned.session.clone(),
|
|
1140
|
+
window_index: Some(0),
|
|
1141
|
+
window_name: Some(spawned.window.clone()),
|
|
1142
|
+
pane_index: Some(0),
|
|
1143
|
+
tty: None,
|
|
1144
|
+
current_command: Some("claude".to_string()),
|
|
1145
|
+
current_path: None,
|
|
1146
|
+
active: true,
|
|
1147
|
+
pane_pid: spawned.child_pid,
|
|
1148
|
+
leader_env: BTreeMap::new(),
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
#[test]
|
|
1153
|
+
fn managed_attach_success_requires_live_provider_pane() {
|
|
1154
|
+
let spawned = managed_spawn_result();
|
|
1155
|
+
let transport = ScriptedTransport::with_liveness(PaneLiveness::Dead);
|
|
1156
|
+
|
|
1157
|
+
let err = ensure_managed_provider_live_after_attach(&transport, &spawned)
|
|
1158
|
+
.expect_err("dead provider pane must fail managed launch");
|
|
1159
|
+
|
|
1160
|
+
let text = err.to_string();
|
|
1161
|
+
assert!(
|
|
1162
|
+
text.contains("managed leader provider pane is not running"),
|
|
1163
|
+
"{text}"
|
|
1164
|
+
);
|
|
1165
|
+
assert!(text.contains("%42"), "{text}");
|
|
1166
|
+
assert!(text.contains("claude_code"), "{text}");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
#[test]
|
|
1170
|
+
fn managed_attach_success_accepts_live_provider_pane() {
|
|
1171
|
+
let spawned = managed_spawn_result();
|
|
1172
|
+
let transport = ScriptedTransport::with_liveness(PaneLiveness::Live);
|
|
1173
|
+
|
|
1174
|
+
ensure_managed_provider_live_after_attach(&transport, &spawned)
|
|
1175
|
+
.expect("live provider pane keeps managed launch successful");
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
#[test]
|
|
1179
|
+
fn managed_attach_success_uses_target_scan_when_liveness_unknown() {
|
|
1180
|
+
let spawned = managed_spawn_result();
|
|
1181
|
+
let transport = ScriptedTransport::with_liveness_and_targets(
|
|
1182
|
+
PaneLiveness::Unknown,
|
|
1183
|
+
vec![managed_pane_info(&spawned)],
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
ensure_managed_provider_live_after_attach(&transport, &spawned)
|
|
1187
|
+
.expect("target scan can prove provider pane is still live");
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1066
1190
|
struct EnvGuard {
|
|
1067
1191
|
saved: Vec<(&'static str, Option<String>)>,
|
|
1068
1192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.36",
|
|
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.3.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.3.36",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.36",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.36"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|