@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.
Files changed (95) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +161 -29
  4. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  5. package/crates/team-agent/src/cli/emit.rs +24 -8
  6. package/crates/team-agent/src/cli/mod.rs +543 -98
  7. package/crates/team-agent/src/cli/models.rs +393 -32
  8. package/crates/team-agent/src/cli/named_address.rs +19 -5
  9. package/crates/team-agent/src/cli/profile.rs +35 -4
  10. package/crates/team-agent/src/cli/spec.rs +7 -2
  11. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  12. package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
  13. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  14. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  15. package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  17. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  18. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  19. package/crates/team-agent/src/cli/types.rs +1 -0
  20. package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
  21. package/crates/team-agent/src/leader/lease.rs +492 -33
  22. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  23. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  24. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  25. package/crates/team-agent/src/leader/start.rs +197 -88
  26. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  27. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  28. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  29. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  30. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  31. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
  32. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  33. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  34. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  35. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  36. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  38. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  39. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  40. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
  42. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  43. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  44. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  45. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  46. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  47. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
  48. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  49. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  50. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  51. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  52. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  53. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  54. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
  55. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  56. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  57. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  58. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  59. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  60. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
  61. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  62. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  63. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  64. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
  65. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
  66. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  67. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  68. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  69. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  70. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  71. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  72. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  73. package/crates/team-agent/src/messaging/delivery.rs +221 -80
  74. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  75. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
  76. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  77. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  78. package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
  79. package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
  80. package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
  81. package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
  82. package/crates/team-agent/src/state/mod.rs +1 -0
  83. package/crates/team-agent/src/state/persist.rs +102 -4
  84. package/crates/team-agent/src/state/projection.rs +7 -1
  85. package/crates/team-agent/src/state/repository.rs +29 -0
  86. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
  87. package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
  88. package/crates/team-agent/src/tmux_backend.rs +239 -127
  89. package/crates/team-agent/src/transport.rs +78 -0
  90. package/crates/team-agent/src/transport_factory.rs +53 -0
  91. package/package.json +4 -5
  92. package/skills/team-agent/SKILL.md +45 -17
  93. package/skills/team-agent/command-coverage.json +4 -29
  94. package/npm/check-skill-links.mjs +0 -88
  95. 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 !leader_receiver_attached(state) {
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
- "team-agent claim-leader",
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(workspace, state, &mut issues, &mut repairs);
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
- fn append_registry_channel_unbound_issue(
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
- state: &Value,
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("leader_not_attached")
301
- || item.get("id").and_then(Value::as_str) == Some("leader_channel_unbound")
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!("leader_not_attached"));
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
- state
309
- .get("session_name")
310
- .and_then(Value::as_str)
311
- .unwrap_or(team_key.as_str()),
312
- "leader_not_attached",
313
- "team-agent claim-leader",
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 => "team-agent attach-leader",
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
- PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane => {
429
- format!(
430
- "open a new terminal window outside the current tmux/pane, change \
431
- directory to the requested workspace, then run \
432
- `team-agent attach-leader --team {team} --provider {provider} \
433
- --confirm --json`; then rerun \
434
- `team-agent diagnose --team {team} --json`"
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
- repairs.push(recovery_hint(
483
- &team_key,
484
- "leader_not_attached",
485
- "team-agent claim-leader",
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": format!(
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
- for spec in COMMAND_SPECS.iter().filter(|spec| spec.default_help) {
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 == spec.name),
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
- assert!(
1997
- visible.len() <= 15,
1998
- "default visible command count must stay <= 15, got {visible:?}"
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"),