@team-agent/installer 0.5.75 → 0.5.78

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 (98) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +184 -125
  6. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  7. package/crates/team-agent/src/cli/emit.rs +59 -15
  8. package/crates/team-agent/src/cli/mod.rs +600 -134
  9. package/crates/team-agent/src/cli/models.rs +485 -44
  10. package/crates/team-agent/src/cli/named_address.rs +19 -5
  11. package/crates/team-agent/src/cli/profile.rs +35 -4
  12. package/crates/team-agent/src/cli/spec.rs +7 -2
  13. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  14. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  15. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  16. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  17. package/crates/team-agent/src/cli/status_port.rs +8 -0
  18. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  19. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  20. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  21. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  22. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  23. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/leader/lease.rs +538 -41
  27. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  28. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  29. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  30. package/crates/team-agent/src/leader/start.rs +151 -14
  31. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  32. package/crates/team-agent/src/leader/types.rs +4 -0
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  35. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  36. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  37. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +138 -14
  38. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  39. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  40. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  41. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  42. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  43. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  44. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  45. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  46. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  47. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1002 -65
  48. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  49. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  50. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  51. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  52. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  53. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  54. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -3
  55. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  56. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  57. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  58. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  59. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  60. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  61. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  62. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  63. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  64. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  65. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  66. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +4 -3
  67. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  68. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  69. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  70. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +69 -28
  71. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  72. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  73. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  74. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  75. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  76. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  77. package/crates/team-agent/src/lifecycle/types.rs +12 -0
  78. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  79. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  80. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  81. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  82. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  83. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  84. package/crates/team-agent/src/state/paths.rs +17 -2
  85. package/crates/team-agent/src/state/persist.rs +97 -1
  86. package/crates/team-agent/src/state/projection.rs +18 -2
  87. package/crates/team-agent/src/state/repository.rs +29 -0
  88. package/crates/team-agent/src/state/selector.rs +23 -3
  89. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
  90. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  91. package/crates/team-agent/src/tmux_backend.rs +208 -118
  92. package/crates/team-agent/src/transport.rs +72 -0
  93. package/crates/team-agent/src/transport_factory.rs +53 -0
  94. package/package.json +4 -5
  95. package/skills/team-agent/SKILL.md +45 -17
  96. package/skills/team-agent/command-coverage.json +4 -29
  97. package/npm/check-skill-links.mjs +0 -88
  98. 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
 
@@ -85,10 +86,31 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
85
86
  }
86
87
  match dispatch(command, &argv[1..], cwd) {
87
88
  Ok(exit) => exit,
88
- Err(error) => emit_cli_error(command, &argv[1..], cwd, &error),
89
+ Err(error) => emit_cli_error_for_command(command, &argv[1..], cwd, &error),
89
90
  }
90
91
  }
91
92
 
93
+ fn emit_cli_error_for_command(
94
+ command: &str,
95
+ args: &[String],
96
+ cwd: &Path,
97
+ error: &CliError,
98
+ ) -> ExitCode {
99
+ if command == "status" {
100
+ emit_status_cli_error(error)
101
+ } else {
102
+ emit_cli_error(command, args, cwd, error)
103
+ }
104
+ }
105
+
106
+ fn emit_status_cli_error(error: &CliError) -> ExitCode {
107
+ let normalized = normalize_cli_error(error);
108
+ let payload_error = normalized.as_ref().unwrap_or(error);
109
+ let safe_error = crate::redaction::redact_external_text(&payload_error.to_string());
110
+ eprintln!("error: {safe_error}");
111
+ ExitCode::Error
112
+ }
113
+
92
114
  /// Print a handler's CmdResult to stdout (emit formats json/human), then surface its exit code.
93
115
  /// (parser.py: `print(emit(result, as_json))` then the ok→exit mapping.)
94
116
  fn emit_result(r: CmdResult) -> ExitCode {
@@ -387,7 +409,7 @@ fn command_help(command: Option<&str>) -> String {
387
409
  )
388
410
  .to_string(),
389
411
  Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
390
- Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n默认输出: worker,空闲|工作|错误;错误细分走 status --summary".to_string(),
412
+ Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影。缺少可靠定位或 nodeprobe 证据时显示 unknown;tmux_command 可复制到对应目标。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
391
413
  Some("models") => "usage: team-agent models --provider pi [--search TEXT] [--json]\n\nPrints models.v1 exact role_model entries; each entry includes current=true|false. Catalog readiness is reported as auth=ok|not_ready with auth_basis=catalog_visibility.".to_string(),
392
414
  Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
393
415
  Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
@@ -416,7 +438,7 @@ fn command_help(command: Option<&str>) -> String {
416
438
  Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
417
439
  Some("validate") => "usage: team-agent validate [SPEC] [--json]".to_string(),
418
440
  Some("install-skill") => "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]".to_string(),
419
- 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(),
420
442
  Some("collect") => "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]".to_string(),
421
443
  Some("results") => "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
422
444
  Some("wait") => "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]".to_string(),
@@ -832,6 +854,7 @@ struct ParsedArgs {
832
854
  assignee: Option<String>,
833
855
  out: Option<PathBuf>,
834
856
  auth_mode: Option<String>,
857
+ proxy_mode: Option<String>,
835
858
  pane: Option<String>,
836
859
  to_name: Option<String>,
837
860
  /// E7 (0.5.9 host-leader-registry-design §4.2): `send --to-leader NAME`
@@ -930,6 +953,7 @@ fn parse_args(args: &[String]) -> ParsedArgs {
930
953
  "--assignee" => parsed.assignee = next_arg(args, &mut i),
931
954
  "--out" => parsed.out = next_arg(args, &mut i).map(PathBuf::from),
932
955
  "--auth-mode" => parsed.auth_mode = next_arg(args, &mut i),
956
+ "--proxy-mode" => parsed.proxy_mode = next_arg(args, &mut i),
933
957
  "--pane" => parsed.pane = next_arg(args, &mut i),
934
958
  "--to-name" => parsed.to_name = next_arg(args, &mut i),
935
959
  "--to-leader" => parsed.to_leader = next_arg(args, &mut i),
@@ -948,6 +972,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
948
972
  other if other.starts_with("--team=") => {
949
973
  parsed.team = Some(other.trim_start_matches("--team=").to_string());
950
974
  }
975
+ other if other.starts_with("--proxy-mode=") => {
976
+ parsed.proxy_mode = Some(other.trim_start_matches("--proxy-mode=").to_string());
977
+ }
951
978
  other if other.starts_with("--pane=") => {
952
979
  parsed.pane = Some(other.trim_start_matches("--pane=").to_string());
953
980
  }
@@ -1682,6 +1709,7 @@ fn profile_args(args: &[String], cwd: &Path) -> Result<ProfileArgs, CliError> {
1682
1709
  workspace: resolve_path(&workspace),
1683
1710
  team: parsed.team,
1684
1711
  auth_mode: parsed.auth_mode,
1712
+ proxy_mode: parsed.proxy_mode,
1685
1713
  json: parsed.json,
1686
1714
  })
1687
1715
  }
@@ -1965,16 +1993,25 @@ mod tests {
1965
1993
  "`{command}` appears in default help without default_help=true"
1966
1994
  );
1967
1995
  }
1968
- 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 {
1969
2002
  assert!(
1970
- visible.iter().any(|command| command == spec.name),
1971
- "`{}` has default_help=true but is missing from top-level help",
1972
- spec.name
2003
+ visible.iter().any(|command| command == spec_name),
2004
+ "`{spec_name}` has default_help=true but is missing from top-level help"
1973
2005
  );
1974
2006
  }
1975
- assert!(
1976
- visible.len() <= 15,
1977
- "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:?}"
1978
2015
  );
1979
2016
  assert!(
1980
2017
  top_help.contains("copilot"),
@@ -2184,11 +2221,18 @@ mod tests {
2184
2221
  }
2185
2222
 
2186
2223
  #[test]
2187
- fn status_help_mentions_summary_for_error_details() {
2188
- assert!(
2189
- command_help(Some("status")).contains("错误细分走 status --summary"),
2190
- "status help must tell users where detailed error classes live"
2191
- );
2224
+ fn status_help_describes_brief_projection_and_unknown_boundary() {
2225
+ let help = command_help(Some("status"));
2226
+ for marker in [
2227
+ "name/provider/runtime_status/activity/health/session_name/tmux_command",
2228
+ "人读与 --json 使用同一投影",
2229
+ "显示 unknown",
2230
+ "tmux_command",
2231
+ "不增加诊断字段",
2232
+ ] {
2233
+ assert!(help.contains(marker), "status help missing {marker}: {help}");
2234
+ }
2235
+ assert!(!help.contains("错误细分走 status --summary"));
2192
2236
  }
2193
2237
 
2194
2238
  #[test]