@team-agent/installer 0.5.64 → 0.5.66

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 (105) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +5 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/emit.rs +15 -0
  6. package/crates/team-agent/src/cli/mod.rs +9 -8
  7. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  8. package/crates/team-agent/src/cli/spec.rs +3 -0
  9. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  10. package/crates/team-agent/src/cli/tests/mod.rs +33 -2
  11. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  12. package/crates/team-agent/src/cli/tests/status_send.rs +13 -3
  13. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  14. package/crates/team-agent/src/cli/types.rs +7 -0
  15. package/crates/team-agent/src/compiler/tests.rs +23 -20
  16. package/crates/team-agent/src/compiler.rs +25 -5
  17. package/crates/team-agent/src/conpty/backend.rs +16 -0
  18. package/crates/team-agent/src/coordinator/health.rs +17 -0
  19. package/crates/team-agent/src/coordinator/steps/abnormal.rs +55 -2
  20. package/crates/team-agent/src/db/migration.rs +2 -1
  21. package/crates/team-agent/src/db/schema.rs +16 -8
  22. package/crates/team-agent/src/leader/lease.rs +1 -2
  23. package/crates/team-agent/src/leader/start.rs +175 -10
  24. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  25. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -2
  26. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -2
  27. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +137 -2
  28. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -2
  29. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +5 -0
  30. package/crates/team-agent/src/lifecycle/launch/spawn.rs +39 -2
  31. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +105 -48
  32. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -2
  33. package/crates/team-agent/src/lifecycle/launch.rs +117 -21
  34. package/crates/team-agent/src/lifecycle/restart/agent.rs +89 -3
  35. package/crates/team-agent/src/lifecycle/restart/common.rs +9 -12
  36. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +59 -15
  37. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  38. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +4 -4
  39. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  40. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  41. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  42. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +24 -32
  43. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  44. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +13 -11
  45. package/crates/team-agent/src/lifecycle/tests/core.rs +0 -18
  46. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  47. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  48. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  49. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  50. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  51. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +2 -2
  52. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +17 -30
  53. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +1 -1
  54. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -4
  55. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
  56. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  57. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  58. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  59. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -1
  60. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  61. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  62. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  63. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +11 -7
  64. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  65. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  66. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  67. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  68. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  69. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  70. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  71. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +1 -1
  72. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  73. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  74. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +1 -1
  75. package/crates/team-agent/src/lifecycle/tests.rs +11 -7
  76. package/crates/team-agent/src/lifecycle/types.rs +0 -2
  77. package/crates/team-agent/src/lifecycle/worker_command_context.rs +115 -22
  78. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  79. package/crates/team-agent/src/messaging/delivery.rs +29 -0
  80. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  81. package/crates/team-agent/src/messaging/leader_receiver.rs +12 -0
  82. package/crates/team-agent/src/messaging/mod.rs +2 -0
  83. package/crates/team-agent/src/messaging/results.rs +5 -0
  84. package/crates/team-agent/src/messaging/send.rs +9 -0
  85. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  86. package/crates/team-agent/src/messaging/tests/runtime.rs +32 -0
  87. package/crates/team-agent/src/messaging/types.rs +2 -0
  88. package/crates/team-agent/src/messaging/wait.rs +366 -0
  89. package/crates/team-agent/src/messaging/watchers.rs +153 -6
  90. package/crates/team-agent/src/model/spec.rs +129 -6
  91. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  92. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  93. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  94. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  95. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  96. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  97. package/crates/team-agent/src/provider/bypass_flags.rs +80 -0
  98. package/crates/team-agent/src/provider/mod.rs +2 -0
  99. package/crates/team-agent/src/tmux_backend.rs +35 -12
  100. package/crates/team-agent/src/transport/test_support.rs +20 -0
  101. package/crates/team-agent/src/transport.rs +28 -11
  102. package/package.json +4 -4
  103. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  104. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  105. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -1064,6 +1064,23 @@ pub fn render_event_line(event: &Value) -> Option<String> {
1064
1064
  clean_field(event, &["provider"], "-"),
1065
1065
  clean_field(event, &["matched_pattern_snippet", "snippet"], "-")
1066
1066
  )),
1067
+ "result_wake.registered" => Some(format!(
1068
+ "result_wake.registered: task={} watcher={}",
1069
+ clean_field(event, &["task_id"], "-"),
1070
+ clean_field(event, &["watcher_id"], "-")
1071
+ )),
1072
+ "result_wake.notified" => Some(format!(
1073
+ "result_wake.notified: task={} result={} watcher={}",
1074
+ clean_field(event, &["task_id"], "-"),
1075
+ clean_field(event, &["result_id"], "-"),
1076
+ clean_field(event, &["watcher_id"], "-")
1077
+ )),
1078
+ "result_wake.notify_failed" => Some(format!(
1079
+ "result_wake.notify_failed: task={} watcher={} reason={}",
1080
+ clean_field(event, &["task_id"], "-"),
1081
+ clean_field(event, &["watcher_id"], "-"),
1082
+ clean_field(event, &["reason", "error"], "-")
1083
+ )),
1067
1084
  _ => None,
1068
1085
  }
1069
1086
  }
@@ -578,7 +578,7 @@ fn agent_process_liveness(
578
578
  // pane for POSITIVE provider evidence via current-command + worker
579
579
  // exit marker. Under the 0.5.39 wrapper, `state.status=running` is
580
580
  // administrative spawn accounting, not runtime truth — a provider
581
- // may have exited into the shell fallback while state still reads
581
+ // may have exited into the inert shell tail while state still reads
582
582
  // running. The marker probe positively proves that case; when it
583
583
  // fires we return Dead here instead of the false Alive further
584
584
  // down.
@@ -795,9 +795,29 @@ fn worker_provider_exit_marker_check(
795
795
  .capture(&target, crate::transport::CaptureRange::Tail(200))
796
796
  .ok()?;
797
797
  if cap.text.contains(&marker) {
798
+ // The wrapper writes its exit code immediately after this marker. A
799
+ // truncated capture can leave the marker without a complete token;
800
+ // keep that case explicit rather than guessing success (or zero).
801
+ // Scope: this detail only covers exits that execute the wrapper's
802
+ // marker printf. SIGKILL, direct pane termination, and host power loss
803
+ // produce no marker; that absence is silent, not `rc=unknown`.
804
+ let rc = cap
805
+ .text
806
+ .split_once(&marker)
807
+ .and_then(|(_, tail)| {
808
+ let token = tail.split_whitespace().next()?;
809
+ let end = tail.find(token)? + token.len();
810
+ tail.as_bytes()
811
+ .get(end)
812
+ .is_some_and(u8::is_ascii_whitespace)
813
+ .then_some(token)
814
+ })
815
+ .and_then(|token| token.parse::<i32>().ok())
816
+ .map(|value| value.to_string())
817
+ .unwrap_or_else(|| "unknown".to_string());
798
818
  Some(process_check(
799
819
  ProcessLiveness::Dead,
800
- format!("worker_provider_exited:{pane_id_str}"),
820
+ format!("worker_provider_exited:{pane_id_str}:rc={rc}"),
801
821
  ))
802
822
  } else {
803
823
  None
@@ -1816,6 +1836,39 @@ mod tests {
1816
1836
  );
1817
1837
  }
1818
1838
 
1839
+ #[test]
1840
+ fn worker_exit_marker_detail_includes_exit_code() {
1841
+ let pane_id = crate::transport::PaneId::new("%marker-rc");
1842
+ let marker = crate::tmux_backend::worker_provider_exit_marker("codex");
1843
+ let transport = crate::transport::test_support::OfflineTransport::new()
1844
+ .with_capture_for_pane(pane_id.as_str(), format!("\n{marker} 23\n"));
1845
+ let mut agent = test_abnormal_agent("/tmp/rollout.jsonl", Some(1), None);
1846
+ agent.pane_id = Some(pane_id.as_str().to_string());
1847
+
1848
+ let check = worker_provider_exit_marker_check(&agent, &transport).expect("marker hit");
1849
+
1850
+ assert_eq!(check.state, ProcessLiveness::Dead);
1851
+ assert_eq!(check.detail, "worker_provider_exited:%marker-rc:rc=23");
1852
+ }
1853
+
1854
+ #[test]
1855
+ fn worker_exit_marker_detail_uses_unknown_for_truncated_exit_code() {
1856
+ let pane_id = crate::transport::PaneId::new("%marker-truncated");
1857
+ let marker = crate::tmux_backend::worker_provider_exit_marker("codex");
1858
+ let transport = crate::transport::test_support::OfflineTransport::new()
1859
+ .with_capture_for_pane(pane_id.as_str(), format!("\n{marker} "));
1860
+ let mut agent = test_abnormal_agent("/tmp/rollout.jsonl", Some(1), None);
1861
+ agent.pane_id = Some(pane_id.as_str().to_string());
1862
+
1863
+ let check = worker_provider_exit_marker_check(&agent, &transport).expect("marker hit");
1864
+
1865
+ assert_eq!(check.state, ProcessLiveness::Dead);
1866
+ assert_eq!(
1867
+ check.detail,
1868
+ "worker_provider_exited:%marker-truncated:rc=unknown"
1869
+ );
1870
+ }
1871
+
1819
1872
  #[test]
1820
1873
  fn abnormal_dead_fresh_error_event_reports_actual_dead_booleans() {
1821
1874
  let dir = temp_abnormal_dir("dead-fresh");
@@ -98,6 +98,7 @@ pub const MANAGED_TABLE_LAYOUTS: &[(&str, &[&str])] = &[
98
98
  "agent_id",
99
99
  "message_id",
100
100
  "leader_id",
101
+ "recipient",
101
102
  "status",
102
103
  "created_at",
103
104
  "completed_at",
@@ -129,7 +130,7 @@ const CREATE_TABLE_TEMPLATES: &[(&str, &str)] = &[
129
130
  ("delivery_tokens", "create table if not exists __TABLE__ (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )"),
130
131
  ("agent_health", "create table if not exists __TABLE__ (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )"),
131
132
  ("peer_allowlist", "create table if not exists __TABLE__ (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )"),
132
- ("result_watchers", "create table if not exists __TABLE__ (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )"),
133
+ ("result_watchers", "create table if not exists __TABLE__ (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )"),
133
134
  ("leader_notification_log", "create table if not exists __TABLE__ (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )"),
134
135
  ];
135
136
 
@@ -13,7 +13,7 @@ use rusqlite::Connection;
13
13
  use crate::db::DbError;
14
14
 
15
15
  /// `schema.py:90`。
16
- pub const SCHEMA_VERSION: i64 = 4;
16
+ pub const SCHEMA_VERSION: i64 = 5;
17
17
 
18
18
  /// 8 张表的 DDL(逐字照搬 `schema.py:initialize_schema` 的内联建表;含 `if not exists`)。
19
19
  /// 顺序与 Python 一致(leader_notification_log 在 ensure 块后创建)。
@@ -23,7 +23,7 @@ const CREATE_SCHEDULED_EVENTS: &str = "create table if not exists scheduled_even
23
23
  const CREATE_DELIVERY_TOKENS: &str = "create table if not exists delivery_tokens (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )";
24
24
  const CREATE_AGENT_HEALTH: &str = "create table if not exists agent_health (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
25
25
  const CREATE_PEER_ALLOWLIST: &str = "create table if not exists peer_allowlist (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )";
26
- const CREATE_RESULT_WATCHERS: &str = "create table if not exists result_watchers (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )";
26
+ const CREATE_RESULT_WATCHERS: &str = "create table if not exists result_watchers (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )";
27
27
  const CREATE_LEADER_NOTIFICATION_LOG: &str = "create table if not exists leader_notification_log (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )";
28
28
  const CREATE_AGENT_HEALTH_NEW: &str = "create table agent_health_new (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
29
29
 
@@ -104,6 +104,7 @@ const RESULT_WATCHER_COLUMNS: &[&str] = &[
104
104
  "agent_id",
105
105
  "message_id",
106
106
  "leader_id",
107
+ "recipient",
107
108
  "status",
108
109
  "created_at",
109
110
  "completed_at",
@@ -297,10 +298,16 @@ pub fn initialize_schema(
297
298
  &tx,
298
299
  "result_watchers",
299
300
  RESULT_WATCHER_COLUMNS,
300
- &[(
301
- "owner_team_id",
302
- "alter table result_watchers add column owner_team_id text",
303
- )],
301
+ &[
302
+ (
303
+ "owner_team_id",
304
+ "alter table result_watchers add column owner_team_id text",
305
+ ),
306
+ (
307
+ "recipient",
308
+ "alter table result_watchers add column recipient text",
309
+ ),
310
+ ],
304
311
  )?;
305
312
  tx.execute(CREATE_LEADER_NOTIFICATION_LOG, [])?;
306
313
  ensure_table_columns(
@@ -360,13 +367,13 @@ mod tests {
360
367
  }
361
368
 
362
369
  #[test]
363
- fn user_version_is_four() {
370
+ fn user_version_is_five() {
364
371
  let conn = fresh();
365
372
  let v: i64 = conn
366
373
  .query_row("pragma user_version", [], |r| r.get(0))
367
374
  .unwrap();
368
375
  assert_eq!(v, SCHEMA_VERSION);
369
- assert_eq!(v, 4);
376
+ assert_eq!(v, 5);
370
377
  }
371
378
 
372
379
  #[test]
@@ -513,6 +520,7 @@ mod tests {
513
520
  ("agent_id", "TEXT", 0, None, 0),
514
521
  ("message_id", "TEXT", 0, None, 0),
515
522
  ("leader_id", "TEXT", 1, None, 0),
523
+ ("recipient", "TEXT", 0, None, 0),
516
524
  ("status", "TEXT", 1, None, 0),
517
525
  ("created_at", "TEXT", 1, None, 0),
518
526
  ("completed_at", "TEXT", 0, None, 0),
@@ -505,7 +505,6 @@ pub fn claim_leader(
505
505
  team: Option<&str>,
506
506
  confirm: bool,
507
507
  ) -> Result<LeaseResult, LeaderError> {
508
- let _ = confirm;
509
508
  let caller = std::env::var("TMUX_PANE")
510
509
  .ok()
511
510
  .filter(|pane| !pane.is_empty())
@@ -598,7 +597,7 @@ pub fn claim_leader(
598
597
  Some(team_id.as_str()),
599
598
  &team_id,
600
599
  &PaneId::new(caller),
601
- true,
600
+ confirm,
602
601
  &event_log,
603
602
  &liveness,
604
603
  caller_target.as_ref(),
@@ -1059,6 +1059,16 @@ fn execute_managed_leader_plan_attempt(
1059
1059
  workspace,
1060
1060
  session_existed_before,
1061
1061
  )?;
1062
+ // A-55 (window rename race): tmux defaults `allow-rename` + `automatic-rename`
1063
+ // ON, so the pane's shell wrapper can print an OSC title-set escape during
1064
+ // init (zshrc/starship/PROMPT_COMMAND) and rename the window away from WINDOW
1065
+ // between spawn-poll and attach — making `attach-session -t SESSION:WINDOW`
1066
+ // fail with "can't find window: WINDOW". Lock both off right after spawn,
1067
+ // before run_leader_argv. Best-effort: a failure must NOT abort the launcher
1068
+ // (old tmux may lack the option), but it is audited below.
1069
+ let (rename_result, rename_stderr) =
1070
+ lock_down_window_rename(&transport, session, &spawned.window);
1071
+ write_window_rename_lockdown_event(workspace, &rename_result, &rename_stderr);
1062
1072
  if let Err(error) = persist_managed_leader_binding(plan, workspace, &spawned) {
1063
1073
  cleanup_managed_leader_resources(
1064
1074
  &transport,
@@ -1269,12 +1279,12 @@ fn ensure_managed_provider_live_after_attach(
1269
1279
  /// 0.4.x (CR C-3 P0): leader provider health reconciliation. The default
1270
1280
  /// `liveness()` check only proves the pane is ADDRESSABLE via tmux — it
1271
1281
  /// returns `Live` even when the provider has exited and the wrapper shell
1272
- /// fell back to an interactive shell. This function distinguishes
1282
+ /// remains at its inert tail. This function distinguishes
1273
1283
  /// `provider_alive` from `provider_exited` by:
1274
1284
  /// 1. Reading `pane_current_command` (tmux `#{pane_current_command}`).
1275
1285
  /// 2. If the current command matches the expected provider binary (or
1276
1286
  /// one of its known aliases), report `Alive`.
1277
- /// 3. If the current command is an interactive shell AND the pane
1287
+ /// 3. If the current command is a shell tail process AND the pane
1278
1288
  /// content contains the exit marker `[team-agent] <provider> exited`
1279
1289
  /// (emitted by `leader_shell_wrapper_command`), report
1280
1290
  /// `ProviderExited`.
@@ -1282,7 +1292,7 @@ fn ensure_managed_provider_live_after_attach(
1282
1292
  /// conservative default — avoid false-positive exit alarms.
1283
1293
  ///
1284
1294
  /// Note: when the leader shell wrapper is used (CR C-2), a provider exit
1285
- /// leaves the pane as `<SHELL:-/bin/zsh>` with the exit marker in
1295
+ /// leaves the pane as an inert `/bin/sh -c` tail with the exit marker in
1286
1296
  /// scrollback. Pre-wrapper code that hit `exec claude` would have left the
1287
1297
  /// pane as `[exited]` and `liveness()` would have returned `Dead`. The new
1288
1298
  /// failure mode requires this richer health check to surface
@@ -1340,16 +1350,16 @@ pub enum LeaderProviderHealth {
1340
1350
  Unreachable,
1341
1351
  }
1342
1352
 
1343
- /// 0.4.x (CR R6 + R3): single-source interactive-shell detection.
1353
+ /// 0.4.x (CR R6 + R3): single-source shell-tail detection.
1344
1354
  /// Used by:
1345
- /// - `leader_provider_health` to decide "pane fell back to shell"
1346
- /// - shutdown logic to recognise a leader pane in fallback-shell mode
1355
+ /// - `leader_provider_health` to decide "pane is at the shell tail"
1356
+ /// - shutdown logic to recognise a leader pane in shell-tail mode
1347
1357
  /// as still owned by the leader (not stray).
1348
1358
  ///
1349
1359
  /// Matches by basename (case-insensitive) — `pane_current_command` returns
1350
1360
  /// the basename of the running binary. Conservative whitelist of POSIX +
1351
- /// common interactive shells; missing entries here are false negatives
1352
- /// (shell looks like provider absent → health says Alive) which is the
1361
+ /// common shell processes; missing entries here are false negatives
1362
+ /// (shell tail looks like provider absent → health says Alive) which is the
1353
1363
  /// safe default per the CR R6 conservative-Alive rule.
1354
1364
  fn is_interactive_shell_basename(name: &str) -> bool {
1355
1365
  let trimmed = name.trim().to_ascii_lowercase();
@@ -2246,6 +2256,44 @@ fn write_leader_startup_prompt_event(workspace: &Path, event: &str, fields: serd
2246
2256
  let _ = crate::event_log::EventLog::new(workspace).write(event, fields);
2247
2257
  }
2248
2258
 
2259
+ /// A-55 (window rename race): lock the freshly-spawned leader window against
2260
+ /// rename — `allow-rename off` (refuse pane-internal OSC title-set renames)
2261
+ /// AND `automatic-rename off` (refuse tmux's own command-driven rename). Both
2262
+ /// must be set; tmux treats them as independent. Best-effort: a failure must
2263
+ /// NOT abort the launcher (old tmux may lack the option) — return the outcome
2264
+ /// for audit instead of propagating an error. Returns (result, tmux_stderr).
2265
+ fn lock_down_window_rename(
2266
+ transport: &dyn Transport,
2267
+ session: &SessionName,
2268
+ window: &WindowName,
2269
+ ) -> (String, String) {
2270
+ let allow_rename = transport.set_window_option(session, window, "allow-rename", "off");
2271
+ let automatic_rename = transport.set_window_option(session, window, "automatic-rename", "off");
2272
+ let (result, stderr) = match (&allow_rename, &automatic_rename) {
2273
+ (Ok(()), Ok(())) => ("ok".to_string(), String::new()),
2274
+ (Ok(()), Err(error)) => ("failed".to_string(), error.to_string()),
2275
+ (Err(error), Ok(())) => ("failed".to_string(), error.to_string()),
2276
+ (Err(a), Err(b)) => (
2277
+ "failed".to_string(),
2278
+ format!("allow-rename: {a}; automatic-rename: {b}"),
2279
+ ),
2280
+ };
2281
+ (result, stderr)
2282
+ }
2283
+
2284
+ /// A-55: audit the rename-lockdown outcome to the event log. Failure does not
2285
+ /// abort the launcher; record ok/failed + tmux stderr for regression triage.
2286
+ fn write_window_rename_lockdown_event(workspace: &Path, result: &str, stderr: &str) {
2287
+ write_leader_startup_prompt_event(
2288
+ workspace,
2289
+ "leader.launcher.window_rename_lockdown",
2290
+ serde_json::json!({
2291
+ "result": result,
2292
+ "tmux_stderr": stderr,
2293
+ }),
2294
+ );
2295
+ }
2296
+
2249
2297
  fn ensure_tmux_installed() -> Result<(), LeaderError> {
2250
2298
  match Command::new("tmux").arg("-V").output() {
2251
2299
  Ok(output) if output.status.success() => Ok(()),
@@ -2350,8 +2398,8 @@ mod tests {
2350
2398
  use super::{
2351
2399
  ensure_managed_provider_live_after_attach, execute_leader_plan_after_ambient_authority,
2352
2400
  format_launcher_failure, handle_exec_provider_startup_prompts,
2353
- is_interactive_shell_basename, push_bounded_stderr, shlex_quote,
2354
- VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
2401
+ is_interactive_shell_basename, lock_down_window_rename, push_bounded_stderr, shlex_quote,
2402
+ write_window_rename_lockdown_event, VerifiedAmbientPaneAuthority, LEADER_STDERR_LIMIT,
2355
2403
  };
2356
2404
 
2357
2405
  struct ScriptedTransport {
@@ -2359,6 +2407,8 @@ mod tests {
2359
2407
  sent: Mutex<Vec<(Target, Vec<Key>)>>,
2360
2408
  liveness: PaneLiveness,
2361
2409
  targets: Vec<PaneInfo>,
2410
+ window_options: Mutex<Vec<(String, String)>>,
2411
+ window_option_error: Mutex<Option<String>>,
2362
2412
  }
2363
2413
 
2364
2414
  impl ScriptedTransport {
@@ -2368,6 +2418,8 @@ mod tests {
2368
2418
  sent: Mutex::new(Vec::new()),
2369
2419
  liveness: PaneLiveness::Unknown,
2370
2420
  targets: Vec::new(),
2421
+ window_options: Mutex::new(Vec::new()),
2422
+ window_option_error: Mutex::new(None),
2371
2423
  }
2372
2424
  }
2373
2425
 
@@ -2377,6 +2429,8 @@ mod tests {
2377
2429
  sent: Mutex::new(Vec::new()),
2378
2430
  liveness,
2379
2431
  targets: Vec::new(),
2432
+ window_options: Mutex::new(Vec::new()),
2433
+ window_option_error: Mutex::new(None),
2380
2434
  }
2381
2435
  }
2382
2436
 
@@ -2386,6 +2440,21 @@ mod tests {
2386
2440
  sent: Mutex::new(Vec::new()),
2387
2441
  liveness,
2388
2442
  targets,
2443
+ window_options: Mutex::new(Vec::new()),
2444
+ window_option_error: Mutex::new(None),
2445
+ }
2446
+ }
2447
+
2448
+ /// A-55: a ScriptedTransport whose `set_window_option` always fails with
2449
+ /// the given tmux stderr — proves the lockdown never aborts the launcher.
2450
+ fn with_window_option_error(error: &str) -> Self {
2451
+ Self {
2452
+ screens: Mutex::new(Vec::new()),
2453
+ sent: Mutex::new(Vec::new()),
2454
+ liveness: PaneLiveness::Unknown,
2455
+ targets: Vec::new(),
2456
+ window_options: Mutex::new(Vec::new()),
2457
+ window_option_error: Mutex::new(Some(error.to_string())),
2389
2458
  }
2390
2459
  }
2391
2460
 
@@ -2395,6 +2464,14 @@ mod tests {
2395
2464
  Err(poisoned) => poisoned.into_inner().clone(),
2396
2465
  }
2397
2466
  }
2467
+
2468
+ /// All `set-window-option option value` pairs, in call order.
2469
+ fn window_options(&self) -> Vec<(String, String)> {
2470
+ match self.window_options.lock() {
2471
+ Ok(guard) => guard.clone(),
2472
+ Err(poisoned) => poisoned.into_inner().clone(),
2473
+ }
2474
+ }
2398
2475
  }
2399
2476
 
2400
2477
  impl Transport for ScriptedTransport {
@@ -2511,6 +2588,34 @@ mod tests {
2511
2588
  Ok(SetEnvOutcome::Applied)
2512
2589
  }
2513
2590
 
2591
+ fn set_window_option(
2592
+ &self,
2593
+ _session: &SessionName,
2594
+ _window: &WindowName,
2595
+ option: &str,
2596
+ value: &str,
2597
+ ) -> Result<(), TransportError> {
2598
+ if let Some(stderr) = self
2599
+ .window_option_error
2600
+ .lock()
2601
+ .ok()
2602
+ .and_then(|guard| guard.clone())
2603
+ {
2604
+ return Err(TransportError::Subprocess {
2605
+ argv: vec!["tmux".to_string(), "set-window-option".to_string()],
2606
+ code: Some(1),
2607
+ stderr,
2608
+ });
2609
+ }
2610
+ match self.window_options.lock() {
2611
+ Ok(mut guard) => guard.push((option.to_string(), value.to_string())),
2612
+ Err(poisoned) => poisoned
2613
+ .into_inner()
2614
+ .push((option.to_string(), value.to_string())),
2615
+ }
2616
+ Ok(())
2617
+ }
2618
+
2514
2619
  fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
2515
2620
  Ok(())
2516
2621
  }
@@ -2988,4 +3093,64 @@ mod tests {
2988
3093
  assert_eq!(captured.len(), LEADER_STDERR_LIMIT);
2989
3094
  assert!(captured.ends_with(b"XYZ"));
2990
3095
  }
3096
+
3097
+ #[test]
3098
+ fn managed_leader_launch_disables_window_rename() {
3099
+ let transport = ScriptedTransport::with_liveness(PaneLiveness::Live);
3100
+ let session = SessionName::new("team-agent-leader-claude_code-demo");
3101
+ let window = WindowName::new("claude_code");
3102
+
3103
+ let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
3104
+
3105
+ assert_eq!(result, "ok", "both set-window-option calls must succeed");
3106
+ assert!(stderr.is_empty());
3107
+ let options = transport.window_options();
3108
+ assert_eq!(
3109
+ options,
3110
+ vec![
3111
+ ("allow-rename".to_string(), "off".to_string()),
3112
+ ("automatic-rename".to_string(), "off".to_string()),
3113
+ ],
3114
+ "both rename lockdown options must be issued, in order"
3115
+ );
3116
+ }
3117
+
3118
+ #[test]
3119
+ fn launcher_survives_set_window_option_failure() {
3120
+ let workspace =
3121
+ std::env::temp_dir().join(format!("ta-rename-lockdown-fail-{}", std::process::id()));
3122
+ std::fs::create_dir_all(&workspace).unwrap();
3123
+ let transport = ScriptedTransport::with_window_option_error(
3124
+ "set-window-option: unknown option: allow-rename",
3125
+ );
3126
+ let session = SessionName::new("team-agent-leader-claude_code-demo");
3127
+ let window = WindowName::new("claude_code");
3128
+
3129
+ // Lockdown must NOT abort the launcher: it returns a failed outcome
3130
+ // (recorded for audit) instead of propagating an error.
3131
+ let (result, stderr) = lock_down_window_rename(&transport, &session, &window);
3132
+
3133
+ assert_eq!(result, "failed", "a set-window-option failure is audited, not fatal");
3134
+ assert!(
3135
+ stderr.contains("unknown option"),
3136
+ "tmux stderr must surface in the audit payload: {stderr}"
3137
+ );
3138
+
3139
+ // The audit event lands in the workspace event log with result=failed.
3140
+ write_window_rename_lockdown_event(&workspace, &result, &stderr);
3141
+ let events = crate::event_log::EventLog::new(&workspace).tail(0).unwrap();
3142
+ let lockdown = events
3143
+ .iter()
3144
+ .find(|event| {
3145
+ event["event"] == "leader.launcher.window_rename_lockdown"
3146
+ })
3147
+ .expect("rename_lockdown event must be written even on failure");
3148
+ assert_eq!(lockdown["result"], "failed");
3149
+ assert!(
3150
+ lockdown["tmux_stderr"].as_str().unwrap_or_default().contains("unknown option"),
3151
+ "event must carry tmux stderr for regression triage: {lockdown}"
3152
+ );
3153
+
3154
+ std::fs::remove_dir_all(&workspace).ok();
3155
+ }
2991
3156
  }
@@ -1054,7 +1054,7 @@ fn team_dir(root: &Path, name: &str, agents: &[(&str, &str)]) -> PathBuf {
1054
1054
 
1055
1055
  fn role_doc(name: &str, role: &str) -> String {
1056
1056
  format!(
1057
- "---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{role}.\n"
1057
+ "---\nname: {name}\nrole: {role}\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\n{role}.\n"
1058
1058
  )
1059
1059
  }
1060
1060
 
@@ -337,7 +337,6 @@ pub(super) fn add_agent_with_transport_at_paths(
337
337
  )));
338
338
  }
339
339
  inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
340
- let safety = effective_runtime_config(&spec)?;
341
340
  // E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
342
341
  let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
343
342
  // E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
@@ -362,7 +361,6 @@ pub(super) fn add_agent_with_transport_at_paths(
362
361
  agent_id,
363
362
  &meta,
364
363
  role_file_path,
365
- &safety,
366
364
  ) {
367
365
  rollback_add_agent_atomic(
368
366
  run_workspace,
@@ -116,7 +116,6 @@ pub(super) fn upsert_agent_state_from_role(
116
116
  agent_id: &AgentId,
117
117
  meta: &Value,
118
118
  dynamic_role_file: &Path,
119
- safety: &DangerousApproval,
120
119
  ) -> Result<(), LifecycleError> {
121
120
  let mut state =
122
121
  crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
@@ -195,7 +194,13 @@ pub(super) fn upsert_agent_state_from_role(
195
194
  }
196
195
  }
197
196
  if let Some(obj) = entry.as_object_mut() {
198
- persist_effective_approval_policy(obj, safety);
197
+ // 0.5.66 bypass 单源:policy 从 role 文档字段派生,不再用全队 `DangerousApproval`。
198
+ let meta_provider = meta
199
+ .get("provider")
200
+ .and_then(Value::as_str)
201
+ .and_then(crate::provider::wire::parse_provider)
202
+ .unwrap_or(Provider::Codex);
203
+ persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
199
204
  }
200
205
  agent_map.insert(agent_id.as_str().to_string(), entry);
201
206
  crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);