@team-agent/installer 0.5.77 → 0.5.80
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 +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +393 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +197 -88
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -103,6 +103,21 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
103
103
|
issues.push(issue);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
if state
|
|
107
|
+
.get("leader_receiver")
|
|
108
|
+
.is_some_and(receiver_has_empty_attached_pane)
|
|
109
|
+
{
|
|
110
|
+
issues.push(json!(LEADER_NOT_ATTACHED));
|
|
111
|
+
repairs.push(recovery_hint(
|
|
112
|
+
state
|
|
113
|
+
.get("session_name")
|
|
114
|
+
.and_then(Value::as_str)
|
|
115
|
+
.unwrap_or("unknown"),
|
|
116
|
+
LEADER_NOT_ATTACHED,
|
|
117
|
+
LEADER_NOT_ATTACHED_REPAIR,
|
|
118
|
+
));
|
|
119
|
+
}
|
|
120
|
+
|
|
106
121
|
if let Some(session_name) = state
|
|
107
122
|
.get("session_name")
|
|
108
123
|
.and_then(Value::as_str)
|
|
@@ -140,17 +155,7 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
|
|
143
|
-
if
|
|
144
|
-
issues.push(json!("leader_not_attached"));
|
|
145
|
-
repairs.push(recovery_hint(
|
|
146
|
-
state
|
|
147
|
-
.get("session_name")
|
|
148
|
-
.and_then(Value::as_str)
|
|
149
|
-
.unwrap_or("unknown"),
|
|
150
|
-
"leader_not_attached",
|
|
151
|
-
"team-agent attach-leader",
|
|
152
|
-
));
|
|
153
|
-
} else {
|
|
158
|
+
if leader_receiver_attached(state) {
|
|
154
159
|
// 0.4.x (CR R2 P0): leader provider health reconciliation. The
|
|
155
160
|
// leader_receiver may be marked `attached` (pane addressable) but the
|
|
156
161
|
// provider process has exited — pane fell back to shell with the exit
|
|
@@ -186,7 +191,7 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
186
191
|
.and_then(Value::as_str)
|
|
187
192
|
.unwrap_or("unknown"),
|
|
188
193
|
"leader_provider_unreachable",
|
|
189
|
-
"
|
|
194
|
+
"leader pane provider is unreachable; restart the provider in the bound pane",
|
|
190
195
|
));
|
|
191
196
|
}
|
|
192
197
|
crate::leader::LeaderProviderHealth::Alive => {}
|
|
@@ -267,6 +272,7 @@ pub(crate) fn diagnose_runtime_for_workspace(
|
|
|
267
272
|
workspace: &std::path::Path,
|
|
268
273
|
state: &Value,
|
|
269
274
|
backend: &dyn Transport,
|
|
275
|
+
selected_team_key: Option<&str>,
|
|
270
276
|
) -> (Value, Value) {
|
|
271
277
|
let (mut issues, mut repairs) = diagnose_runtime(state, backend);
|
|
272
278
|
append_live_leader_workspace_mismatch_issue(
|
|
@@ -276,43 +282,159 @@ pub(crate) fn diagnose_runtime_for_workspace(
|
|
|
276
282
|
&mut issues,
|
|
277
283
|
&mut repairs,
|
|
278
284
|
);
|
|
279
|
-
append_registry_channel_unbound_issue(
|
|
285
|
+
append_registry_channel_unbound_issue(
|
|
286
|
+
workspace,
|
|
287
|
+
state,
|
|
288
|
+
selected_team_key,
|
|
289
|
+
&mut issues,
|
|
290
|
+
&mut repairs,
|
|
291
|
+
);
|
|
280
292
|
append_legacy_snapshot_issue(workspace, state, &mut issues);
|
|
281
293
|
append_coordinator_health_issue(workspace, state, &mut issues, &mut repairs);
|
|
282
294
|
append_runtime_bindings_stale_after_boot_issue(workspace, state, &mut issues, &mut repairs);
|
|
283
295
|
(issues, repairs)
|
|
284
296
|
}
|
|
285
297
|
|
|
286
|
-
|
|
298
|
+
const TEAM_SPEC_OR_RUNTIME_MISSING: &str = "team_spec_or_runtime_missing";
|
|
299
|
+
const TEAM_SPEC_OR_RUNTIME_MISSING_REPAIR: &str = concat!(
|
|
300
|
+
"no team spec or runtime was found in this workspace. ",
|
|
301
|
+
"Run `team-agent quick-start <workspace>` for first launch, ",
|
|
302
|
+
"or restart from a workspace that already contains `.team`.",
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
pub(crate) fn workspace_has_existing_team_runtime(
|
|
287
306
|
workspace: &std::path::Path,
|
|
288
|
-
|
|
307
|
+
team_key: &str,
|
|
308
|
+
) -> bool {
|
|
309
|
+
workspace.join("team.spec.yaml").is_file()
|
|
310
|
+
|| crate::model::paths::runtime_spec_path(workspace, team_key).is_file()
|
|
311
|
+
|| crate::state::persist::runtime_state_path(workspace).is_file()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
pub(crate) fn missing_team_runtime_issues_and_repairs(team_key: &str) -> (Value, Value) {
|
|
315
|
+
let mut issues = json!([]);
|
|
316
|
+
let mut repairs = json!([]);
|
|
317
|
+
push_issue_repair(
|
|
318
|
+
&mut issues,
|
|
319
|
+
&mut repairs,
|
|
320
|
+
team_key,
|
|
321
|
+
TEAM_SPEC_OR_RUNTIME_MISSING,
|
|
322
|
+
TEAM_SPEC_OR_RUNTIME_MISSING_REPAIR,
|
|
323
|
+
);
|
|
324
|
+
(issues, repairs)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
fn push_issue_repair(
|
|
289
328
|
issues: &mut Value,
|
|
290
329
|
repairs: &mut Value,
|
|
330
|
+
team: &str,
|
|
331
|
+
issue_id: &str,
|
|
332
|
+
repair: &str,
|
|
291
333
|
) {
|
|
292
|
-
let team_key = crate::state::projection::team_state_key(state);
|
|
293
|
-
if team_key.is_empty()
|
|
294
|
-
|| crate::lifecycle::launch::launched_team_receiver_is_attached(workspace, &team_key)
|
|
295
|
-
{
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
334
|
if let Some(items) = issues.as_array_mut() {
|
|
299
335
|
if !items.iter().any(|item| {
|
|
300
|
-
item.as_str() == Some(
|
|
301
|
-
|| item.get("id").and_then(Value::as_str) == Some(
|
|
336
|
+
item.as_str() == Some(issue_id)
|
|
337
|
+
|| item.get("id").and_then(Value::as_str) == Some(issue_id)
|
|
302
338
|
}) {
|
|
303
|
-
items.push(json!(
|
|
339
|
+
items.push(json!(issue_id));
|
|
304
340
|
}
|
|
305
341
|
}
|
|
306
342
|
if let Some(items) = repairs.as_array_mut() {
|
|
307
|
-
items.push(recovery_hint(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
343
|
+
items.push(recovery_hint(team, issue_id, repair));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fn append_registry_channel_unbound_issue(
|
|
348
|
+
workspace: &std::path::Path,
|
|
349
|
+
state: &Value,
|
|
350
|
+
selected_team_key: Option<&str>,
|
|
351
|
+
issues: &mut Value,
|
|
352
|
+
repairs: &mut Value,
|
|
353
|
+
) {
|
|
354
|
+
let team_key = selected_team_key
|
|
355
|
+
.filter(|team| !team.is_empty())
|
|
356
|
+
.map(str::to_string)
|
|
357
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(state));
|
|
358
|
+
if team_key.is_empty() {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// RuntimeOnly diagnose synthesizes team_key=current from empty default
|
|
362
|
+
// state. That is not an existing unbound team; do not emit claim.
|
|
363
|
+
if !workspace_has_existing_team_runtime(workspace, &team_key) {
|
|
364
|
+
push_issue_repair(
|
|
365
|
+
issues,
|
|
366
|
+
repairs,
|
|
367
|
+
&team_key,
|
|
368
|
+
TEAM_SPEC_OR_RUNTIME_MISSING,
|
|
369
|
+
TEAM_SPEC_OR_RUNTIME_MISSING_REPAIR,
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
let class = crate::lifecycle::launch::classify_leader_binding(workspace, &team_key);
|
|
374
|
+
if class == crate::lifecycle::launch::LeaderBindingClass::Attached {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
let (issue_id, repair) = existing_team_binding_issue(state, &team_key, class);
|
|
378
|
+
push_issue_repair(
|
|
379
|
+
issues,
|
|
380
|
+
repairs,
|
|
381
|
+
state
|
|
382
|
+
.get("session_name")
|
|
383
|
+
.and_then(Value::as_str)
|
|
384
|
+
.unwrap_or(team_key.as_str()),
|
|
385
|
+
issue_id,
|
|
386
|
+
repair,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const LEADER_NOT_ATTACHED: &str = "leader_not_attached";
|
|
391
|
+
const LEADER_NOT_ATTACHED_REPAIR: &str =
|
|
392
|
+
"run from the intended leader tmux pane; do not run claim-leader";
|
|
393
|
+
|
|
394
|
+
fn receiver_has_empty_attached_pane(receiver: &Value) -> bool {
|
|
395
|
+
receiver.get("status").and_then(Value::as_str) == Some("attached")
|
|
396
|
+
&& receiver
|
|
397
|
+
.get("pane_id")
|
|
398
|
+
.and_then(Value::as_str)
|
|
399
|
+
.is_none_or(|pane| pane.is_empty())
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fn selected_or_top_receiver<'a>(state: &'a Value, team_key: &str) -> Option<&'a Value> {
|
|
403
|
+
crate::lifecycle::launch::selected_team_leader_receiver(state, team_key)
|
|
404
|
+
.or_else(|| state.get("leader_receiver"))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
fn team_has_initialized_runtime(state: &Value, team_key: &str) -> bool {
|
|
408
|
+
let team = state
|
|
409
|
+
.get("teams")
|
|
410
|
+
.and_then(Value::as_object)
|
|
411
|
+
.and_then(|teams| teams.get(team_key))
|
|
412
|
+
.unwrap_or(state);
|
|
413
|
+
let has_agents = team
|
|
414
|
+
.get("agents")
|
|
415
|
+
.and_then(Value::as_object)
|
|
416
|
+
.is_some_and(|agents| !agents.is_empty())
|
|
417
|
+
|| state
|
|
418
|
+
.get("agents")
|
|
419
|
+
.and_then(Value::as_object)
|
|
420
|
+
.is_some_and(|agents| !agents.is_empty());
|
|
421
|
+
let has_owner = team.get("team_owner").is_some() || state.get("team_owner").is_some();
|
|
422
|
+
let has_receiver = selected_or_top_receiver(state, team_key).is_some();
|
|
423
|
+
has_agents || has_owner || has_receiver
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fn existing_team_binding_issue(
|
|
427
|
+
state: &Value,
|
|
428
|
+
team_key: &str,
|
|
429
|
+
class: crate::lifecycle::launch::LeaderBindingClass,
|
|
430
|
+
) -> (&'static str, &'static str) {
|
|
431
|
+
if selected_or_top_receiver(state, team_key).is_some_and(receiver_has_empty_attached_pane)
|
|
432
|
+
|| (class == crate::lifecycle::launch::LeaderBindingClass::Unbound
|
|
433
|
+
&& team_has_initialized_runtime(state, team_key))
|
|
434
|
+
{
|
|
435
|
+
return (LEADER_NOT_ATTACHED, LEADER_NOT_ATTACHED_REPAIR);
|
|
315
436
|
}
|
|
437
|
+
(class.issue_id(), class.repair())
|
|
316
438
|
}
|
|
317
439
|
|
|
318
440
|
fn append_live_leader_workspace_mismatch_issue(
|
|
@@ -353,8 +475,7 @@ fn live_leader_workspace_mismatch(
|
|
|
353
475
|
let provider = receiver
|
|
354
476
|
.get("provider")
|
|
355
477
|
.and_then(Value::as_str)
|
|
356
|
-
.filter(|provider| !provider.is_empty())
|
|
357
|
-
.unwrap_or("claude");
|
|
478
|
+
.filter(|provider| !provider.is_empty());
|
|
358
479
|
let refusal =
|
|
359
480
|
PaneAuthorityRefusal::new(PaneAuthorityRefusalFacts::PaneWorkspaceMismatch(facts));
|
|
360
481
|
let PaneAuthorityRefusalFacts::PaneWorkspaceMismatch(facts) = &refusal.facts else {
|
|
@@ -413,7 +534,9 @@ fn live_leader_workspace_mismatch(
|
|
|
413
534
|
HINT_ACTION_FIELD.to_string(),
|
|
414
535
|
Value::String(
|
|
415
536
|
match refusal.recovery.hint_action {
|
|
416
|
-
PaneAuthorityRecoveryHint::AttachLeader =>
|
|
537
|
+
PaneAuthorityRecoveryHint::AttachLeader => {
|
|
538
|
+
"open a matching workspace pane bound for this workspace"
|
|
539
|
+
}
|
|
417
540
|
}
|
|
418
541
|
.to_string(),
|
|
419
542
|
),
|
|
@@ -424,15 +547,19 @@ fn live_leader_workspace_mismatch(
|
|
|
424
547
|
);
|
|
425
548
|
object.insert(
|
|
426
549
|
ACTION_FIELD.to_string(),
|
|
427
|
-
Value::String(match refusal.recovery.action {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
550
|
+
Value::String(match (refusal.recovery.action, provider) {
|
|
551
|
+
(
|
|
552
|
+
PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane,
|
|
553
|
+
Some(provider),
|
|
554
|
+
) => format!(
|
|
555
|
+
"open a matching workspace pane already bound as {provider}"
|
|
556
|
+
),
|
|
557
|
+
(
|
|
558
|
+
PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane,
|
|
559
|
+
None,
|
|
560
|
+
) => {
|
|
561
|
+
"open a matching workspace pane with the recorded leader provider; do not default claude"
|
|
562
|
+
.to_string()
|
|
436
563
|
}
|
|
437
564
|
}),
|
|
438
565
|
);
|
|
@@ -454,9 +581,7 @@ pub(crate) fn append_registry_channel_unbound_to_report(
|
|
|
454
581
|
.map(|state| crate::state::projection::team_state_key(&state))
|
|
455
582
|
})
|
|
456
583
|
.unwrap_or_default();
|
|
457
|
-
if team_key.is_empty()
|
|
458
|
-
|| crate::lifecycle::launch::launched_team_receiver_is_attached(workspace, &team_key)
|
|
459
|
-
{
|
|
584
|
+
if team_key.is_empty() {
|
|
460
585
|
return;
|
|
461
586
|
}
|
|
462
587
|
let Some(object) = report.as_object_mut() else {
|
|
@@ -467,23 +592,37 @@ pub(crate) fn append_registry_channel_unbound_to_report(
|
|
|
467
592
|
.and_then(Value::as_array)
|
|
468
593
|
.cloned()
|
|
469
594
|
.unwrap_or_default();
|
|
470
|
-
if !issues.iter().any(|item| {
|
|
471
|
-
item.as_str() == Some("leader_not_attached")
|
|
472
|
-
|| item.get("id").and_then(Value::as_str) == Some("leader_channel_unbound")
|
|
473
|
-
}) {
|
|
474
|
-
issues.push(json!("leader_not_attached"));
|
|
475
|
-
}
|
|
476
|
-
object.insert("issues".to_string(), Value::Array(issues));
|
|
477
595
|
let mut repairs = object
|
|
478
596
|
.get("suggested_repairs")
|
|
479
597
|
.and_then(Value::as_array)
|
|
480
598
|
.cloned()
|
|
481
599
|
.unwrap_or_default();
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
600
|
+
if !workspace_has_existing_team_runtime(workspace, &team_key) {
|
|
601
|
+
let mut issues_value = Value::Array(issues);
|
|
602
|
+
let mut repairs_value = Value::Array(repairs);
|
|
603
|
+
push_issue_repair(
|
|
604
|
+
&mut issues_value,
|
|
605
|
+
&mut repairs_value,
|
|
606
|
+
&team_key,
|
|
607
|
+
TEAM_SPEC_OR_RUNTIME_MISSING,
|
|
608
|
+
TEAM_SPEC_OR_RUNTIME_MISSING_REPAIR,
|
|
609
|
+
);
|
|
610
|
+
object.insert("issues".to_string(), issues_value);
|
|
611
|
+
object.insert("suggested_repairs".to_string(), repairs_value);
|
|
612
|
+
object.insert("ok".to_string(), Value::Bool(false));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
let class = crate::lifecycle::launch::classify_leader_binding(workspace, &team_key);
|
|
616
|
+
if class == crate::lifecycle::launch::LeaderBindingClass::Attached {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
let state = crate::state::persist::load_runtime_state(workspace).unwrap_or(json!({}));
|
|
620
|
+
let (issue_id, repair) = existing_team_binding_issue(&state, &team_key, class);
|
|
621
|
+
if !issues.iter().any(|item| item.as_str() == Some(issue_id)) {
|
|
622
|
+
issues.push(json!(issue_id));
|
|
623
|
+
}
|
|
624
|
+
repairs.push(recovery_hint(&team_key, issue_id, repair));
|
|
625
|
+
object.insert("issues".to_string(), Value::Array(issues));
|
|
487
626
|
object.insert("suggested_repairs".to_string(), Value::Array(repairs));
|
|
488
627
|
}
|
|
489
628
|
|
|
@@ -798,9 +937,7 @@ fn recovery_hint(team: &str, broken_class: &str, hint_action: &str) -> Value {
|
|
|
798
937
|
"broken_class": broken_class,
|
|
799
938
|
"hint_action": hint_action,
|
|
800
939
|
"dedupe_key": format!("{team}:{broken_class}"),
|
|
801
|
-
"action":
|
|
802
|
-
"{hint_action} # alternatives: team-agent restart; team-agent claim-leader; team-agent takeover; team-agent quick-start; team-agent attach-leader"
|
|
803
|
-
),
|
|
940
|
+
"action": hint_action,
|
|
804
941
|
})
|
|
805
942
|
}
|
|
806
943
|
|
|
@@ -808,6 +945,140 @@ fn recovery_hint(team: &str, broken_class: &str, hint_action: &str) -> Value {
|
|
|
808
945
|
mod tests {
|
|
809
946
|
use super::*;
|
|
810
947
|
|
|
948
|
+
#[test]
|
|
949
|
+
fn hint_action_does_not_emit_attach_leader() {
|
|
950
|
+
let workspace = std::path::PathBuf::from("/tmp/ta-hint-ws");
|
|
951
|
+
let endpoint = "/tmp/ta-hint.sock";
|
|
952
|
+
let state = json!({
|
|
953
|
+
"team_key": "alpha",
|
|
954
|
+
"leader_receiver": {
|
|
955
|
+
"status": "attached",
|
|
956
|
+
"pane_id": "%1",
|
|
957
|
+
"provider": "codex",
|
|
958
|
+
"tmux_socket": endpoint
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
let transport = crate::transport::test_support::OfflineTransport::default()
|
|
962
|
+
.with_tmux_endpoint(endpoint)
|
|
963
|
+
.with_targets(vec![crate::transport::PaneInfo {
|
|
964
|
+
pane_id: crate::transport::PaneId::new("%1"),
|
|
965
|
+
session: crate::transport::SessionName::new("s"),
|
|
966
|
+
window_index: None,
|
|
967
|
+
window_name: None,
|
|
968
|
+
pane_index: None,
|
|
969
|
+
tty: None,
|
|
970
|
+
current_command: Some("codex".to_string()),
|
|
971
|
+
current_path: Some(std::path::PathBuf::from("/tmp/other-ws")),
|
|
972
|
+
active: true,
|
|
973
|
+
pane_pid: None,
|
|
974
|
+
leader_env: Default::default(),
|
|
975
|
+
}]);
|
|
976
|
+
let Some((_, repair)) =
|
|
977
|
+
live_leader_workspace_mismatch(&workspace, &state, &transport)
|
|
978
|
+
else {
|
|
979
|
+
panic!("workspace mismatch repair required");
|
|
980
|
+
};
|
|
981
|
+
let hint = repair
|
|
982
|
+
.get(HINT_ACTION_FIELD)
|
|
983
|
+
.and_then(Value::as_str)
|
|
984
|
+
.unwrap_or("");
|
|
985
|
+
let action = repair
|
|
986
|
+
.get(ACTION_FIELD)
|
|
987
|
+
.and_then(Value::as_str)
|
|
988
|
+
.unwrap_or("");
|
|
989
|
+
for field in [hint, action] {
|
|
990
|
+
assert!(
|
|
991
|
+
!field.contains("attach-leader"),
|
|
992
|
+
"mismatch repair must not emit attach-leader; got {field}"
|
|
993
|
+
);
|
|
994
|
+
assert!(
|
|
995
|
+
!field.contains("claim-leader"),
|
|
996
|
+
"mismatch repair must not emit claim-leader; got {field}"
|
|
997
|
+
);
|
|
998
|
+
assert!(
|
|
999
|
+
!field.contains("takeover"),
|
|
1000
|
+
"mismatch repair must not emit takeover; got {field}"
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
#[test]
|
|
1006
|
+
fn recovery_hint_does_not_bundle_unrelated_bind_commands() {
|
|
1007
|
+
let hint = recovery_hint("alpha", "tmux_session_missing", "team-agent restart");
|
|
1008
|
+
let action = hint.get("action").and_then(Value::as_str).unwrap_or("");
|
|
1009
|
+
assert_eq!(action, "team-agent restart");
|
|
1010
|
+
assert!(
|
|
1011
|
+
!action.contains("claim-leader"),
|
|
1012
|
+
"shotgun alternatives must not appear; action={action}"
|
|
1013
|
+
);
|
|
1014
|
+
assert!(
|
|
1015
|
+
!action.contains("takeover"),
|
|
1016
|
+
"shotgun alternatives must not appear; action={action}"
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#[test]
|
|
1021
|
+
fn missing_workspace_does_not_classify_as_unbound_claim() {
|
|
1022
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
1023
|
+
"ta-diagnose-missing-{}-{}",
|
|
1024
|
+
std::process::id(),
|
|
1025
|
+
std::time::SystemTime::now()
|
|
1026
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
1027
|
+
.unwrap()
|
|
1028
|
+
.as_nanos()
|
|
1029
|
+
));
|
|
1030
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
1031
|
+
let state = json!({"agents": {}, "tasks": [], "session_name": null, "active_team_key": null});
|
|
1032
|
+
let mut issues = json!([]);
|
|
1033
|
+
let mut repairs = json!([]);
|
|
1034
|
+
append_registry_channel_unbound_issue(
|
|
1035
|
+
&workspace,
|
|
1036
|
+
&state,
|
|
1037
|
+
Some("current"),
|
|
1038
|
+
&mut issues,
|
|
1039
|
+
&mut repairs,
|
|
1040
|
+
);
|
|
1041
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
1042
|
+
let issues_text = issues.to_string();
|
|
1043
|
+
let repairs_text = repairs.to_string();
|
|
1044
|
+
assert!(
|
|
1045
|
+
issues_text.contains(TEAM_SPEC_OR_RUNTIME_MISSING),
|
|
1046
|
+
"issues={issues_text}"
|
|
1047
|
+
);
|
|
1048
|
+
assert!(
|
|
1049
|
+
!issues_text.contains("leader_receiver_unbound"),
|
|
1050
|
+
"missing team must not be unbound; issues={issues_text}"
|
|
1051
|
+
);
|
|
1052
|
+
assert!(
|
|
1053
|
+
repairs_text.contains("quick-start"),
|
|
1054
|
+
"repairs={repairs_text}"
|
|
1055
|
+
);
|
|
1056
|
+
assert!(
|
|
1057
|
+
!repairs_text.contains("claim-leader"),
|
|
1058
|
+
"missing team must not induce claim; repairs={repairs_text}"
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
#[test]
|
|
1063
|
+
fn diagnose_runtime_does_not_emit_not_committed_parallel() {
|
|
1064
|
+
let state = json!({
|
|
1065
|
+
"leader_receiver": {"status": "pending", "pane_id": "%1"}
|
|
1066
|
+
});
|
|
1067
|
+
let transport = crate::transport::test_support::OfflineTransport::default();
|
|
1068
|
+
let (issues, repairs) = diagnose_runtime(&state, &transport);
|
|
1069
|
+
let issues_text = issues.to_string();
|
|
1070
|
+
let repairs_text = repairs.to_string();
|
|
1071
|
+
assert!(
|
|
1072
|
+
!issues_text.contains("leader_receiver_not_committed"),
|
|
1073
|
+
"issues={issues_text}"
|
|
1074
|
+
);
|
|
1075
|
+
assert!(
|
|
1076
|
+
!repairs_text.contains("leader_receiver_not_committed"),
|
|
1077
|
+
"repairs={repairs_text}"
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
|
|
811
1082
|
fn write_codex_identity_rollout(
|
|
812
1083
|
workspace: &std::path::Path,
|
|
813
1084
|
session_id: &str,
|
|
@@ -1492,11 +1763,6 @@ fn fake_agent_started(agent: &Value) -> bool {
|
|
|
1492
1763
|
}
|
|
1493
1764
|
|
|
1494
1765
|
fn agent_mcp_ready(agent: &Value) -> bool {
|
|
1495
|
-
if agent.get("provider").and_then(Value::as_str) == Some("pi") {
|
|
1496
|
-
// The generated wrapper proves configured-lazy only. No persisted
|
|
1497
|
-
// objective Pi MCP roundtrip receipt exists on the readiness path yet.
|
|
1498
|
-
return false;
|
|
1499
|
-
}
|
|
1500
1766
|
agent
|
|
1501
1767
|
.get("mcp_config")
|
|
1502
1768
|
.and_then(Value::as_str)
|
|
@@ -1640,7 +1906,6 @@ pub(crate) fn provider_doctor_checks() -> Value {
|
|
|
1640
1906
|
crate::provider::Provider::GeminiCli,
|
|
1641
1907
|
crate::provider::Provider::Grok,
|
|
1642
1908
|
crate::provider::Provider::CursorAgent,
|
|
1643
|
-
crate::provider::Provider::Pi,
|
|
1644
1909
|
crate::provider::Provider::Fake,
|
|
1645
1910
|
] {
|
|
1646
1911
|
let adapter = crate::provider::get_adapter(provider);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//! 人读标量/集合渲染(`human_value` / `json_dumps_like`)。
|
|
4
4
|
|
|
5
5
|
use super::spec::{command_spec, CommandKind, CommandTier, ALL_DISPATCH_KINDS, COMMAND_SPECS};
|
|
6
|
+
use super::models::cmd_models;
|
|
6
7
|
use super::*;
|
|
7
8
|
use std::io::{ErrorKind, Write as _};
|
|
8
9
|
|
|
@@ -437,7 +438,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
437
438
|
Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
438
439
|
Some("validate") => "usage: team-agent validate [SPEC] [--json]".to_string(),
|
|
439
440
|
Some("install-skill") => "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]".to_string(),
|
|
440
|
-
Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]".to_string(),
|
|
441
|
+
Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]".to_string(),
|
|
441
442
|
Some("collect") => "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]".to_string(),
|
|
442
443
|
Some("results") => "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
443
444
|
Some("wait") => "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]".to_string(),
|
|
@@ -853,6 +854,7 @@ struct ParsedArgs {
|
|
|
853
854
|
assignee: Option<String>,
|
|
854
855
|
out: Option<PathBuf>,
|
|
855
856
|
auth_mode: Option<String>,
|
|
857
|
+
proxy_mode: Option<String>,
|
|
856
858
|
pane: Option<String>,
|
|
857
859
|
to_name: Option<String>,
|
|
858
860
|
/// E7 (0.5.9 host-leader-registry-design §4.2): `send --to-leader NAME`
|
|
@@ -951,6 +953,7 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
951
953
|
"--assignee" => parsed.assignee = next_arg(args, &mut i),
|
|
952
954
|
"--out" => parsed.out = next_arg(args, &mut i).map(PathBuf::from),
|
|
953
955
|
"--auth-mode" => parsed.auth_mode = next_arg(args, &mut i),
|
|
956
|
+
"--proxy-mode" => parsed.proxy_mode = next_arg(args, &mut i),
|
|
954
957
|
"--pane" => parsed.pane = next_arg(args, &mut i),
|
|
955
958
|
"--to-name" => parsed.to_name = next_arg(args, &mut i),
|
|
956
959
|
"--to-leader" => parsed.to_leader = next_arg(args, &mut i),
|
|
@@ -969,6 +972,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
969
972
|
other if other.starts_with("--team=") => {
|
|
970
973
|
parsed.team = Some(other.trim_start_matches("--team=").to_string());
|
|
971
974
|
}
|
|
975
|
+
other if other.starts_with("--proxy-mode=") => {
|
|
976
|
+
parsed.proxy_mode = Some(other.trim_start_matches("--proxy-mode=").to_string());
|
|
977
|
+
}
|
|
972
978
|
other if other.starts_with("--pane=") => {
|
|
973
979
|
parsed.pane = Some(other.trim_start_matches("--pane=").to_string());
|
|
974
980
|
}
|
|
@@ -1703,6 +1709,7 @@ fn profile_args(args: &[String], cwd: &Path) -> Result<ProfileArgs, CliError> {
|
|
|
1703
1709
|
workspace: resolve_path(&workspace),
|
|
1704
1710
|
team: parsed.team,
|
|
1705
1711
|
auth_mode: parsed.auth_mode,
|
|
1712
|
+
proxy_mode: parsed.proxy_mode,
|
|
1706
1713
|
json: parsed.json,
|
|
1707
1714
|
})
|
|
1708
1715
|
}
|
|
@@ -1986,16 +1993,25 @@ mod tests {
|
|
|
1986
1993
|
"`{command}` appears in default help without default_help=true"
|
|
1987
1994
|
);
|
|
1988
1995
|
}
|
|
1989
|
-
|
|
1996
|
+
let expected: Vec<&str> = COMMAND_SPECS
|
|
1997
|
+
.iter()
|
|
1998
|
+
.filter(|spec| spec.default_help)
|
|
1999
|
+
.map(|spec| spec.name)
|
|
2000
|
+
.collect();
|
|
2001
|
+
for spec_name in &expected {
|
|
1990
2002
|
assert!(
|
|
1991
|
-
visible.iter().any(|command| command ==
|
|
1992
|
-
"`{}` has default_help=true but is missing from top-level help"
|
|
1993
|
-
spec.name
|
|
2003
|
+
visible.iter().any(|command| command == spec_name),
|
|
2004
|
+
"`{spec_name}` has default_help=true but is missing from top-level help"
|
|
1994
2005
|
);
|
|
1995
2006
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2007
|
+
let mut actual = visible.clone();
|
|
2008
|
+
actual.sort();
|
|
2009
|
+
let mut expected_sorted: Vec<String> =
|
|
2010
|
+
expected.iter().map(|name| (*name).to_string()).collect();
|
|
2011
|
+
expected_sorted.sort();
|
|
2012
|
+
assert_eq!(
|
|
2013
|
+
actual, expected_sorted,
|
|
2014
|
+
"default help must match the exact default_help spec set (d40 ∪ results when present), not a slack threshold; got {visible:?}"
|
|
1999
2015
|
);
|
|
2000
2016
|
assert!(
|
|
2001
2017
|
top_help.contains("copilot"),
|