@team-agent/installer 0.3.13 → 0.3.15
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 +1 -0
- package/crates/team-agent/src/cli/emit.rs +92 -11
- package/crates/team-agent/src/cli/mod.rs +228 -85
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
- package/crates/team-agent/src/cli/types.rs +30 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/fake_worker.rs +32 -145
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +692 -92
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
- package/crates/team-agent/src/lifecycle/types.rs +5 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +153 -3
- package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
- package/crates/team-agent/src/messaging/mod.rs +6 -3
- package/crates/team-agent/src/messaging/results.rs +240 -158
- package/crates/team-agent/src/messaging/send.rs +3 -2
- package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
- package/crates/team-agent/src/os_probe.rs +11 -0
- package/crates/team-agent/src/state/persist.rs +6 -0
- package/crates/team-agent/src/tmux_backend.rs +90 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +31 -0
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +277 -0
|
@@ -37,6 +37,120 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
37
37
|
Ok(CmdResult::from_json(value, args.json))
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
pub fn cmd_fallback_send_leader(args: &FallbackSendLeaderArgs) -> Result<CmdResult, CliError> {
|
|
41
|
+
if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
|
|
42
|
+
return Ok(value);
|
|
43
|
+
}
|
|
44
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
45
|
+
&args.workspace,
|
|
46
|
+
args.team.as_deref(),
|
|
47
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
48
|
+
)?;
|
|
49
|
+
let target = MessageTarget::Single("leader".to_string());
|
|
50
|
+
let opts = SendOptions {
|
|
51
|
+
task_id: args.task.as_ref().map(|task| TaskId::new(task.clone())),
|
|
52
|
+
route_task_id: false,
|
|
53
|
+
sender: args.sender.clone(),
|
|
54
|
+
requires_ack: false,
|
|
55
|
+
wait_visible: false,
|
|
56
|
+
block_until_delivered: false,
|
|
57
|
+
team: Some(TeamKey::new(selected.team_key.clone())),
|
|
58
|
+
message_id: Some(args.message_id.clone()),
|
|
59
|
+
..SendOptions::default()
|
|
60
|
+
};
|
|
61
|
+
let primary = messaging::send_message(&selected.run_workspace, &target, &args.content, &opts);
|
|
62
|
+
let message_id = match &primary {
|
|
63
|
+
Ok(outcome) => outcome
|
|
64
|
+
.message_id
|
|
65
|
+
.clone()
|
|
66
|
+
.unwrap_or_else(|| args.message_id.clone()),
|
|
67
|
+
Err(_) => args.message_id.clone(),
|
|
68
|
+
};
|
|
69
|
+
if let Ok(outcome) = &primary {
|
|
70
|
+
if is_business_refusal_outcome(outcome) {
|
|
71
|
+
let value = json!({
|
|
72
|
+
"ok": false,
|
|
73
|
+
"status": "refused",
|
|
74
|
+
"reason": "business_reject",
|
|
75
|
+
"primary_error": args.primary_error,
|
|
76
|
+
"message_id": outcome.message_id,
|
|
77
|
+
"action": "N38 fallback refused: business rule refusals must not use fallback pane delivery",
|
|
78
|
+
});
|
|
79
|
+
return Ok(CmdResult::from_json(value, args.json));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let state = selected_state_with_active_key(&selected);
|
|
84
|
+
let event_log = crate::event_log::EventLog::new(&selected.run_workspace);
|
|
85
|
+
let primary_error = match primary {
|
|
86
|
+
Ok(outcome) if primary_delivery_succeeded(outcome.status) => {
|
|
87
|
+
let mut value = delivery_outcome_json(&outcome, &target, &args.content, &opts);
|
|
88
|
+
if let Some(obj) = value.as_object_mut() {
|
|
89
|
+
obj.insert("fallback_used".to_string(), json!(false));
|
|
90
|
+
obj.insert("primary_error".to_string(), json!(args.primary_error));
|
|
91
|
+
}
|
|
92
|
+
return Ok(CmdResult::from_json(value, args.json));
|
|
93
|
+
}
|
|
94
|
+
Ok(outcome) => format!(
|
|
95
|
+
"{}; fallback_cli_primary_status={}",
|
|
96
|
+
args.primary_error,
|
|
97
|
+
delivery_status_wire(outcome.status)
|
|
98
|
+
),
|
|
99
|
+
Err(error) => format!("{}; fallback_cli_primary_error={error}", args.primary_error),
|
|
100
|
+
};
|
|
101
|
+
let outcome = messaging::deliver_to_leader_fallback_pane(
|
|
102
|
+
&selected.run_workspace,
|
|
103
|
+
&state,
|
|
104
|
+
&message_id,
|
|
105
|
+
None,
|
|
106
|
+
&args.content,
|
|
107
|
+
false,
|
|
108
|
+
Some(&primary_error),
|
|
109
|
+
&event_log,
|
|
110
|
+
)?;
|
|
111
|
+
let mut value = delivery_outcome_json(&outcome, &target, &args.content, &opts);
|
|
112
|
+
if let Some(obj) = value.as_object_mut() {
|
|
113
|
+
obj.insert("primary_error".to_string(), json!(args.primary_error));
|
|
114
|
+
obj.insert("delivered_via".to_string(), json!("fallback_pane"));
|
|
115
|
+
obj.insert(
|
|
116
|
+
"next_action".to_string(),
|
|
117
|
+
json!("run team-agent restart-agent to refresh the worker MCP transport"),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
Ok(CmdResult::from_json(value, args.json))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pub fn cmd_fallback_report_result(args: &FallbackReportResultArgs) -> Result<CmdResult, CliError> {
|
|
124
|
+
if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
|
|
125
|
+
return Ok(value);
|
|
126
|
+
}
|
|
127
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
128
|
+
&args.workspace,
|
|
129
|
+
args.team.as_deref(),
|
|
130
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
131
|
+
)?;
|
|
132
|
+
let envelope = fallback_result_envelope(args)?;
|
|
133
|
+
let value = messaging::report_result_for_owner_team_with_primary_error(
|
|
134
|
+
&selected.run_workspace,
|
|
135
|
+
&envelope,
|
|
136
|
+
Some(&selected.team_key),
|
|
137
|
+
Some(&args.primary_error),
|
|
138
|
+
)?;
|
|
139
|
+
let mut value = value;
|
|
140
|
+
if let Some(obj) = value.as_object_mut() {
|
|
141
|
+
obj.insert("primary_error".to_string(), json!(args.primary_error));
|
|
142
|
+
obj.insert(
|
|
143
|
+
"fallback_protocol".to_string(),
|
|
144
|
+
json!("fallback-report-result"),
|
|
145
|
+
);
|
|
146
|
+
obj.insert(
|
|
147
|
+
"next_action".to_string(),
|
|
148
|
+
json!("run team-agent restart-agent to refresh the worker MCP transport"),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
Ok(CmdResult::from_json(value, args.json))
|
|
152
|
+
}
|
|
153
|
+
|
|
40
154
|
fn routing_ambiguous_value(
|
|
41
155
|
workspace: &Path,
|
|
42
156
|
args: &SendArgs,
|
|
@@ -77,6 +191,101 @@ fn routing_ambiguous_value(
|
|
|
77
191
|
}))
|
|
78
192
|
}
|
|
79
193
|
|
|
194
|
+
fn selected_state_with_active_key(selected: &crate::state::selector::SelectedTeam) -> Value {
|
|
195
|
+
let mut state = selected.state.clone();
|
|
196
|
+
if let Some(obj) = state.as_object_mut() {
|
|
197
|
+
obj.insert(
|
|
198
|
+
"active_team_key".to_string(),
|
|
199
|
+
Value::String(selected.team_key.clone()),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
state
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn fallback_result_envelope(args: &FallbackReportResultArgs) -> Result<Value, CliError> {
|
|
206
|
+
let mut envelope: Value = serde_json::from_str(&args.result_json)?;
|
|
207
|
+
let Some(obj) = envelope.as_object_mut() else {
|
|
208
|
+
return Err(CliError::Usage(
|
|
209
|
+
"--result-json must be a JSON object".to_string(),
|
|
210
|
+
));
|
|
211
|
+
};
|
|
212
|
+
obj.entry("schema_version".to_string())
|
|
213
|
+
.or_insert_with(|| json!("result_envelope_v1"));
|
|
214
|
+
obj.entry("task_id".to_string())
|
|
215
|
+
.or_insert_with(|| json!(args.task_id));
|
|
216
|
+
obj.entry("agent_id".to_string())
|
|
217
|
+
.or_insert_with(|| json!(args.agent_id));
|
|
218
|
+
obj.entry("status".to_string())
|
|
219
|
+
.or_insert_with(|| json!("success"));
|
|
220
|
+
obj.entry("summary".to_string())
|
|
221
|
+
.or_insert_with(|| json!("completed"));
|
|
222
|
+
for key in ["changes", "tests", "risks", "artifacts", "next_actions"] {
|
|
223
|
+
obj.entry(key.to_string()).or_insert_with(|| json!([]));
|
|
224
|
+
}
|
|
225
|
+
Ok(envelope)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn fallback_business_refusal(primary_error: &str, as_json: bool) -> Option<CmdResult> {
|
|
229
|
+
is_business_reject_text(primary_error).then(|| {
|
|
230
|
+
CmdResult::from_json(
|
|
231
|
+
json!({
|
|
232
|
+
"ok": false,
|
|
233
|
+
"status": "refused",
|
|
234
|
+
"reason": "business_reject",
|
|
235
|
+
"primary_error": primary_error,
|
|
236
|
+
"action": "N38 fallback refused: business rule refusals must not use fallback pane delivery",
|
|
237
|
+
}),
|
|
238
|
+
as_json,
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn is_business_refusal_outcome(outcome: &DeliveryOutcome) -> bool {
|
|
244
|
+
matches!(
|
|
245
|
+
outcome.reason,
|
|
246
|
+
Some(
|
|
247
|
+
DeliveryRefusal::TargetNotInTeam
|
|
248
|
+
| DeliveryRefusal::HumanConfirmationRequired
|
|
249
|
+
| DeliveryRefusal::MissingPermissions
|
|
250
|
+
| DeliveryRefusal::UnknownRecipient
|
|
251
|
+
| DeliveryRefusal::TeamOwnerMismatch
|
|
252
|
+
| DeliveryRefusal::Ambiguous
|
|
253
|
+
| DeliveryRefusal::RecipientPaneInNonInputMode
|
|
254
|
+
| DeliveryRefusal::SessionDrift
|
|
255
|
+
| DeliveryRefusal::RoutingAmbiguous
|
|
256
|
+
| DeliveryRefusal::EmptyTargetList
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn primary_delivery_succeeded(status: DeliveryStatus) -> bool {
|
|
262
|
+
matches!(
|
|
263
|
+
status,
|
|
264
|
+
DeliveryStatus::Delivered | DeliveryStatus::AlreadyDelivered
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn is_business_reject_text(error: &str) -> bool {
|
|
269
|
+
let lower = error.to_ascii_lowercase();
|
|
270
|
+
[
|
|
271
|
+
"peer_not_in_scope",
|
|
272
|
+
"target_not_in_team",
|
|
273
|
+
"permission denied",
|
|
274
|
+
"missing_permissions",
|
|
275
|
+
"human_confirmation_required",
|
|
276
|
+
"unknown_recipient",
|
|
277
|
+
"routing_ambiguous",
|
|
278
|
+
"quota",
|
|
279
|
+
"rate limit",
|
|
280
|
+
"rate_limit",
|
|
281
|
+
"blacklist",
|
|
282
|
+
"blacklisted",
|
|
283
|
+
"forbidden",
|
|
284
|
+
]
|
|
285
|
+
.iter()
|
|
286
|
+
.any(|needle| lower.contains(needle))
|
|
287
|
+
}
|
|
288
|
+
|
|
80
289
|
/// `_send_target`(`commands.py:181-184`):`--to` comma-split fanout / `target` 单值 / None。
|
|
81
290
|
pub fn send_target(targets: Option<&str>, target: Option<&str>) -> MessageTarget {
|
|
82
291
|
if let Some(targets) = targets.filter(|s| !s.is_empty()) {
|
|
@@ -221,3 +430,46 @@ fn delivery_stage_wire(stage: DeliveryStage) -> &'static str {
|
|
|
221
430
|
DeliveryStage::VisibleCheck => "visible_check",
|
|
222
431
|
}
|
|
223
432
|
}
|
|
433
|
+
|
|
434
|
+
#[cfg(test)]
|
|
435
|
+
mod e23_tests {
|
|
436
|
+
use super::*;
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn fallback_error_classifier_allows_transport_and_primary_bugs() {
|
|
440
|
+
for error in [
|
|
441
|
+
"Transport closed",
|
|
442
|
+
"Connection refused",
|
|
443
|
+
"Broken pipe",
|
|
444
|
+
"EOF on transport",
|
|
445
|
+
"MCP timeout after 5s",
|
|
446
|
+
"internal assertion failed: unwrap on Err",
|
|
447
|
+
"primary_delivery_error: serialize failed",
|
|
448
|
+
] {
|
|
449
|
+
assert!(
|
|
450
|
+
!is_business_reject_text(error),
|
|
451
|
+
"failure should be fallback-eligible, not classified as a business refusal: {error}"
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#[test]
|
|
457
|
+
fn fallback_error_classifier_blocks_business_refusals() {
|
|
458
|
+
for error in [
|
|
459
|
+
"peer_not_in_scope",
|
|
460
|
+
"target_not_in_team",
|
|
461
|
+
"permission denied",
|
|
462
|
+
"missing_permissions",
|
|
463
|
+
"human_confirmation_required",
|
|
464
|
+
"unknown_recipient",
|
|
465
|
+
"quota exceeded",
|
|
466
|
+
"rate_limit",
|
|
467
|
+
"blacklisted target",
|
|
468
|
+
] {
|
|
469
|
+
assert!(
|
|
470
|
+
is_business_reject_text(error),
|
|
471
|
+
"business refusal must not use fallback pane delivery: {error}"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
@@ -206,6 +206,7 @@ fn current_uid() -> Option<String> {
|
|
|
206
206
|
team_id: None,
|
|
207
207
|
yes: true,
|
|
208
208
|
fresh: false,
|
|
209
|
+
no_display: false,
|
|
209
210
|
json: true,
|
|
210
211
|
};
|
|
211
212
|
let _ = cmd_quick_start(&args); // real quick_start compiles the spec before any coordinator/launch step
|
|
@@ -240,6 +241,7 @@ fn current_uid() -> Option<String> {
|
|
|
240
241
|
team_id: None,
|
|
241
242
|
yes: false,
|
|
242
243
|
fresh: false,
|
|
244
|
+
no_display: false,
|
|
243
245
|
json: true,
|
|
244
246
|
};
|
|
245
247
|
let text = outcome_text(cmd_quick_start(&args));
|
|
@@ -275,15 +277,25 @@ fn current_uid() -> Option<String> {
|
|
|
275
277
|
);
|
|
276
278
|
}
|
|
277
279
|
|
|
278
|
-
// 4 [P1] — cmd_add_agent with a DUPLICATE agent id surfaces the REAL
|
|
279
|
-
// "agent id already exists" error
|
|
280
|
-
//
|
|
280
|
+
// 4 [P1] — cmd_add_agent with a DUPLICATE runtime agent id surfaces the REAL
|
|
281
|
+
// crate::lifecycle::add_agent "agent id already exists" error. E25: role docs/spec files are
|
|
282
|
+
// inputs, not membership; duplicate membership is runtime-state based.
|
|
281
283
|
#[test]
|
|
282
284
|
fn cli_add_agent_duplicate_id_surfaces_real_error() {
|
|
283
285
|
let team = deleg_uniq_dir("addagent");
|
|
284
286
|
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
285
287
|
std::fs::write(team.join("TEAM.md"), DELEG_TEAM_MD).unwrap();
|
|
286
288
|
std::fs::write(team.join("agents").join("implementer.md"), DELEG_VALID_ROLE).unwrap(); // 'implementer' already exists
|
|
289
|
+
crate::state::persist::save_runtime_state(
|
|
290
|
+
&team,
|
|
291
|
+
&json!({
|
|
292
|
+
"session_name": "team-deleg-addagent",
|
|
293
|
+
"agents": {
|
|
294
|
+
"implementer": {"status": "running", "provider": "codex", "window": "implementer"}
|
|
295
|
+
}
|
|
296
|
+
}),
|
|
297
|
+
)
|
|
298
|
+
.unwrap();
|
|
287
299
|
let dup_role = team.join("dup-role.md");
|
|
288
300
|
std::fs::write(&dup_role, DELEG_VALID_ROLE).unwrap(); // role file must exist
|
|
289
301
|
let args = AddAgentArgs {
|
|
@@ -158,11 +158,33 @@ fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
|
|
|
158
158
|
|
|
159
159
|
struct CleanShutdownTransport {
|
|
160
160
|
session_present: Mutex<bool>,
|
|
161
|
+
targets: Vec<PaneInfo>,
|
|
162
|
+
kill_server_called: Mutex<bool>,
|
|
163
|
+
probe_timeout_kind: Option<&'static str>,
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
impl CleanShutdownTransport {
|
|
164
167
|
fn new() -> Self {
|
|
165
|
-
Self {
|
|
168
|
+
Self {
|
|
169
|
+
session_present: Mutex::new(true),
|
|
170
|
+
targets: Vec::new(),
|
|
171
|
+
kill_server_called: Mutex::new(false),
|
|
172
|
+
probe_timeout_kind: None,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn with_targets(mut self, targets: Vec<PaneInfo>) -> Self {
|
|
177
|
+
self.targets = targets;
|
|
178
|
+
self
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn kill_server_called(&self) -> bool {
|
|
182
|
+
*self.kill_server_called.lock().unwrap()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn with_probe_timeout(mut self, probe: &'static str) -> Self {
|
|
186
|
+
self.probe_timeout_kind = Some(probe);
|
|
187
|
+
self
|
|
166
188
|
}
|
|
167
189
|
}
|
|
168
190
|
|
|
@@ -227,7 +249,11 @@ impl Transport for CleanShutdownTransport {
|
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
230
|
-
|
|
252
|
+
if *self.session_present.lock().unwrap() {
|
|
253
|
+
Ok(self.targets.clone())
|
|
254
|
+
} else {
|
|
255
|
+
Ok(Vec::new())
|
|
256
|
+
}
|
|
231
257
|
}
|
|
232
258
|
|
|
233
259
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
@@ -248,10 +274,18 @@ impl Transport for CleanShutdownTransport {
|
|
|
248
274
|
}
|
|
249
275
|
|
|
250
276
|
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
277
|
+
if let Some(probe) = self.probe_timeout_kind {
|
|
278
|
+
crate::os_probe::set_probe_timeout_for_test(probe, None, 900);
|
|
279
|
+
}
|
|
251
280
|
*self.session_present.lock().unwrap() = false;
|
|
252
281
|
Ok(())
|
|
253
282
|
}
|
|
254
283
|
|
|
284
|
+
fn kill_server(&self) -> Result<(), TransportError> {
|
|
285
|
+
*self.kill_server_called.lock().unwrap() = true;
|
|
286
|
+
Ok(())
|
|
287
|
+
}
|
|
288
|
+
|
|
255
289
|
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
256
290
|
Ok(())
|
|
257
291
|
}
|
|
@@ -260,3 +294,126 @@ impl Transport for CleanShutdownTransport {
|
|
|
260
294
|
Ok(AttachOutcome::Attached)
|
|
261
295
|
}
|
|
262
296
|
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
|
|
300
|
+
let ws = tmp_shutdown_workspace("lsof-cwd-timeout-diagnostic");
|
|
301
|
+
crate::state::persist::save_runtime_state(
|
|
302
|
+
&ws,
|
|
303
|
+
&json!({
|
|
304
|
+
"session_name": "team-lsof-cwd-timeout",
|
|
305
|
+
"agents": {
|
|
306
|
+
"fake_impl": {
|
|
307
|
+
"status": "running",
|
|
308
|
+
"provider": "fake",
|
|
309
|
+
"window": "fake_impl"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}),
|
|
313
|
+
)
|
|
314
|
+
.unwrap();
|
|
315
|
+
|
|
316
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
317
|
+
&ws,
|
|
318
|
+
true,
|
|
319
|
+
None,
|
|
320
|
+
&CleanShutdownTransport::new().with_probe_timeout("lsof_cwd"),
|
|
321
|
+
)
|
|
322
|
+
.expect("shutdown should complete");
|
|
323
|
+
|
|
324
|
+
assert_eq!(out["ok"], json!(true));
|
|
325
|
+
assert_eq!(out["status"], json!("ok"));
|
|
326
|
+
assert_eq!(out["phase"], json!(null));
|
|
327
|
+
assert_eq!(out["verification_degraded"], json!(true));
|
|
328
|
+
assert_eq!(out["probe_timeout_kind"], json!("lsof_cwd"));
|
|
329
|
+
assert_eq!(out["residuals"]["sessions"], json!([]));
|
|
330
|
+
assert_eq!(out["residuals"]["processes"], json!([]));
|
|
331
|
+
|
|
332
|
+
let events = crate::event_log::EventLog::new(&ws).tail(0).expect("events");
|
|
333
|
+
let shutdown = events
|
|
334
|
+
.iter()
|
|
335
|
+
.find(|event| {
|
|
336
|
+
event.get("event").and_then(serde_json::Value::as_str) == Some("lifecycle.shutdown")
|
|
337
|
+
})
|
|
338
|
+
.unwrap_or_else(|| panic!("missing lifecycle.shutdown event: {events:?}"));
|
|
339
|
+
assert_eq!(shutdown["status"], json!("ok"));
|
|
340
|
+
assert_eq!(shutdown["phase"], json!(null));
|
|
341
|
+
assert_eq!(shutdown["verification_degraded"], json!(true));
|
|
342
|
+
assert_eq!(shutdown["probe_timeout_kind"], json!("lsof_cwd"));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[test]
|
|
346
|
+
fn ps_table_timeout_still_degrades_shutdown_truth() {
|
|
347
|
+
let ws = tmp_shutdown_workspace("ps-table-timeout-partial");
|
|
348
|
+
crate::state::persist::save_runtime_state(
|
|
349
|
+
&ws,
|
|
350
|
+
&json!({
|
|
351
|
+
"session_name": "team-ps-table-timeout",
|
|
352
|
+
"agents": {
|
|
353
|
+
"fake_impl": {
|
|
354
|
+
"status": "running",
|
|
355
|
+
"provider": "fake",
|
|
356
|
+
"window": "fake_impl"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
.unwrap();
|
|
362
|
+
|
|
363
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
364
|
+
&ws,
|
|
365
|
+
true,
|
|
366
|
+
None,
|
|
367
|
+
&CleanShutdownTransport::new().with_probe_timeout("ps_table"),
|
|
368
|
+
)
|
|
369
|
+
.expect("shutdown should complete");
|
|
370
|
+
|
|
371
|
+
assert_eq!(out["ok"], json!(false));
|
|
372
|
+
assert_eq!(out["status"], json!("partial"));
|
|
373
|
+
assert_eq!(out["phase"], json!("os_probe"));
|
|
374
|
+
assert_eq!(out["verification_degraded"], json!(true));
|
|
375
|
+
assert_eq!(out["probe_timeout_kind"], json!("ps_table"));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#[test]
|
|
379
|
+
fn leader_env_tmux_socket_never_kills_server_even_when_sessions_look_exclusive() {
|
|
380
|
+
let ws = tmp_shutdown_workspace("leader-env-socket-no-kill-server");
|
|
381
|
+
crate::state::persist::save_runtime_state(
|
|
382
|
+
&ws,
|
|
383
|
+
&json!({
|
|
384
|
+
"tmux_socket_source": "leader_env",
|
|
385
|
+
"agents": {
|
|
386
|
+
"fake_impl": {
|
|
387
|
+
"status": "running",
|
|
388
|
+
"provider": "fake",
|
|
389
|
+
"window": "fake_impl"
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}),
|
|
393
|
+
)
|
|
394
|
+
.unwrap();
|
|
395
|
+
let transport = CleanShutdownTransport::new().with_targets(vec![PaneInfo {
|
|
396
|
+
pane_id: PaneId::new("%1"),
|
|
397
|
+
session: SessionName::new("team-layout"),
|
|
398
|
+
window_index: Some(0),
|
|
399
|
+
window_name: Some(WindowName::new("team-w1")),
|
|
400
|
+
pane_index: Some(0),
|
|
401
|
+
tty: None,
|
|
402
|
+
current_command: None,
|
|
403
|
+
current_path: None,
|
|
404
|
+
active: true,
|
|
405
|
+
pane_pid: None,
|
|
406
|
+
leader_env: BTreeMap::new(),
|
|
407
|
+
}]);
|
|
408
|
+
|
|
409
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
410
|
+
.expect("shutdown should complete");
|
|
411
|
+
|
|
412
|
+
assert_eq!(out["ok"], json!(true));
|
|
413
|
+
assert_eq!(out["killed_sessions"], json!(["team-layout"]));
|
|
414
|
+
assert_eq!(out["spared_sessions"], json!([]));
|
|
415
|
+
assert!(
|
|
416
|
+
!transport.kill_server_called(),
|
|
417
|
+
"leader-env/shared socket shutdown must kill sessions individually, never kill-server"
|
|
418
|
+
);
|
|
419
|
+
}
|
|
@@ -97,7 +97,8 @@ impl CliError {
|
|
|
97
97
|
"tmux session `{session}` already exists. It may be an active team. Do not terminate existing tmux sessions from startup; use a different team name or runtime.session_name and start again."
|
|
98
98
|
);
|
|
99
99
|
payload.next_actions = Some(vec![
|
|
100
|
-
"Use a different team name or runtime.session_name before starting again."
|
|
100
|
+
"Use a different team name or runtime.session_name before starting again."
|
|
101
|
+
.to_string(),
|
|
101
102
|
]);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -262,6 +263,7 @@ pub struct QuickStartArgs {
|
|
|
262
263
|
pub team_id: Option<String>,
|
|
263
264
|
pub yes: bool,
|
|
264
265
|
pub fresh: bool,
|
|
266
|
+
pub no_display: bool,
|
|
265
267
|
pub json: bool,
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -304,6 +306,33 @@ pub struct SendArgs {
|
|
|
304
306
|
pub message_id: Option<String>,
|
|
305
307
|
}
|
|
306
308
|
|
|
309
|
+
/// E23 worker-side emergency fallback for `team_orchestrator.send_message`
|
|
310
|
+
/// transport failures. This is not a general control-plane send path.
|
|
311
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
312
|
+
pub struct FallbackSendLeaderArgs {
|
|
313
|
+
pub workspace: PathBuf,
|
|
314
|
+
pub team: Option<String>,
|
|
315
|
+
pub sender: String,
|
|
316
|
+
pub task: Option<String>,
|
|
317
|
+
pub message_id: String,
|
|
318
|
+
pub content: String,
|
|
319
|
+
pub primary_error: String,
|
|
320
|
+
pub json: bool,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// E23 worker-side emergency fallback for `team_orchestrator.report_result`
|
|
324
|
+
/// transport failures. It must still persist through the results DB.
|
|
325
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
326
|
+
pub struct FallbackReportResultArgs {
|
|
327
|
+
pub workspace: PathBuf,
|
|
328
|
+
pub team: Option<String>,
|
|
329
|
+
pub agent_id: String,
|
|
330
|
+
pub task_id: String,
|
|
331
|
+
pub result_json: String,
|
|
332
|
+
pub primary_error: String,
|
|
333
|
+
pub json: bool,
|
|
334
|
+
}
|
|
335
|
+
|
|
307
336
|
/// `allow-peer-talk`(`parser.py`): allow direct peer communication between two agents.
|
|
308
337
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
309
338
|
pub struct AllowPeerTalkArgs {
|
|
@@ -179,7 +179,7 @@ fn front_matter_non_object_errors() {
|
|
|
179
179
|
// separators=(",",":"))` with the workspace path templated to __WS__.
|
|
180
180
|
// (team-agent-public v0.2.11, /tmp/probe_compiler.py.)
|
|
181
181
|
|
|
182
|
-
const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"
|
|
182
|
+
const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
183
183
|
|
|
184
184
|
#[test]
|
|
185
185
|
fn compile_base_noprofile_matches_python_dict_order_and_values() {
|
|
@@ -352,7 +352,7 @@ tools:
|
|
|
352
352
|
Bravo body.
|
|
353
353
|
";
|
|
354
354
|
|
|
355
|
-
const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"
|
|
355
|
+
const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
356
356
|
|
|
357
357
|
#[test]
|
|
358
358
|
fn compile_two_agents_sorted_by_filename_with_routing_and_startup_order() {
|
|
@@ -247,7 +247,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
247
247
|
"display_backend",
|
|
248
248
|
Value::Str(
|
|
249
249
|
string_field(&team_meta, "display_backend")
|
|
250
|
-
.unwrap_or_else(|| "
|
|
250
|
+
.unwrap_or_else(|| "adaptive".to_string()),
|
|
251
251
|
),
|
|
252
252
|
),
|
|
253
253
|
("session_name", Value::Str(session_name(&team_meta, &team_name))),
|