@team-agent/installer 0.5.41 → 0.5.43

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 (152) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +27 -7
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/coordinator/tick.rs +19 -1
  51. package/crates/team-agent/src/db/message_store.rs +138 -30
  52. package/crates/team-agent/src/db/migration.rs +249 -61
  53. package/crates/team-agent/src/db/schema.rs +303 -82
  54. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  55. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  56. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  57. package/crates/team-agent/src/event_log.rs +70 -16
  58. package/crates/team-agent/src/layout/manager.rs +15 -4
  59. package/crates/team-agent/src/layout/mod.rs +4 -4
  60. package/crates/team-agent/src/layout/overlay.rs +10 -3
  61. package/crates/team-agent/src/layout/placement.rs +5 -1
  62. package/crates/team-agent/src/layout/recovery.rs +4 -2
  63. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  64. package/crates/team-agent/src/layout/sessions.rs +17 -9
  65. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  66. package/crates/team-agent/src/layout/worker_env.rs +52 -19
  67. package/crates/team-agent/src/leader/helpers.rs +7 -1
  68. package/crates/team-agent/src/leader/lease.rs +195 -82
  69. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/start.rs +75 -54
  73. package/crates/team-agent/src/leader/takeover.rs +46 -11
  74. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  75. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  76. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  77. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  78. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  79. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  80. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  81. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  82. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  83. package/crates/team-agent/src/lib.rs +4 -4
  84. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  85. package/crates/team-agent/src/lifecycle/launch.rs +6 -1
  86. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  87. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  88. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  89. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  90. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  91. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  92. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  93. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  94. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  95. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  96. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  97. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  98. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  99. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  100. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  101. package/crates/team-agent/src/main.rs +4 -4
  102. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  103. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  104. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  105. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  106. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  107. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  108. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  109. package/crates/team-agent/src/messaging/mod.rs +2 -3
  110. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  111. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  112. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  113. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  114. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  115. package/crates/team-agent/src/messaging/trust.rs +25 -2
  116. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  117. package/crates/team-agent/src/model/enums.rs +65 -16
  118. package/crates/team-agent/src/model/ids.rs +12 -3
  119. package/crates/team-agent/src/model/paths.rs +28 -7
  120. package/crates/team-agent/src/model/permissions.rs +176 -33
  121. package/crates/team-agent/src/model/routing.rs +66 -20
  122. package/crates/team-agent/src/model/spec.rs +365 -69
  123. package/crates/team-agent/src/model/task_graph.rs +36 -9
  124. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  125. package/crates/team-agent/src/model/yaml.rs +7 -6
  126. package/crates/team-agent/src/packaging/install.rs +23 -9
  127. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  128. package/crates/team-agent/src/packaging/mod.rs +9 -1
  129. package/crates/team-agent/src/packaging/repair.rs +13 -6
  130. package/crates/team-agent/src/packaging/tests.rs +63 -16
  131. package/crates/team-agent/src/packaging/types.rs +22 -7
  132. package/crates/team-agent/src/platform/argv.rs +4 -1
  133. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  134. package/crates/team-agent/src/platform/process.rs +21 -22
  135. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  136. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  137. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  138. package/crates/team-agent/src/provider/classify.rs +127 -42
  139. package/crates/team-agent/src/provider/faults.rs +17 -5
  140. package/crates/team-agent/src/provider/helpers.rs +6 -5
  141. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  142. package/crates/team-agent/src/state/repository.rs +27 -14
  143. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  144. package/crates/team-agent/src/tmux_backend.rs +80 -44
  145. package/crates/team-agent/src/topology.rs +27 -20
  146. package/crates/team-agent/src/transport/test_support.rs +20 -23
  147. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  148. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  149. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  150. package/crates/team-agent/src/transport.rs +12 -17
  151. package/crates/team-agent/src/transport_factory.rs +29 -14
  152. package/package.json +4 -4
@@ -114,7 +114,9 @@ fn is_stripped(key: &str) -> bool {
114
114
 
115
115
  fn is_posix_shell_identifier(s: &str) -> bool {
116
116
  let mut bytes = s.bytes();
117
- let Some(first) = bytes.next() else { return false };
117
+ let Some(first) = bytes.next() else {
118
+ return false;
119
+ };
118
120
  if !(first.is_ascii_alphabetic() || first == b'_') {
119
121
  return false;
120
122
  }
@@ -147,12 +149,24 @@ mod tests {
147
149
  ("OPENAI_API_KEY".to_string(), "sk-test".to_string()),
148
150
  ("COPILOT_TOKEN".to_string(), "tok".to_string()),
149
151
  // Leader identity — must be stripped.
150
- ("TEAM_AGENT_LEADER_PROVIDER".to_string(), "claude".to_string()),
151
- ("TEAM_AGENT_LEADER_SESSION_UUID".to_string(), "leader-uuid".to_string()),
152
- ("TEAM_AGENT_MACHINE_FINGERPRINT".to_string(), "fp".to_string()),
152
+ (
153
+ "TEAM_AGENT_LEADER_PROVIDER".to_string(),
154
+ "claude".to_string(),
155
+ ),
156
+ (
157
+ "TEAM_AGENT_LEADER_SESSION_UUID".to_string(),
158
+ "leader-uuid".to_string(),
159
+ ),
160
+ (
161
+ "TEAM_AGENT_MACHINE_FINGERPRINT".to_string(),
162
+ "fp".to_string(),
163
+ ),
153
164
  ("TEAM_AGENT_TEAM_ID".to_string(), "alpha".to_string()),
154
165
  ("TEAM_AGENT_LEADER_BYPASS".to_string(), "1".to_string()),
155
- ("COPILOT_DISABLE_TERMINAL_TITLE".to_string(), "1".to_string()),
166
+ (
167
+ "COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
168
+ "1".to_string(),
169
+ ),
156
170
  // Random noise — non-whitelisted prefix.
157
171
  ("RANDOM_NOISE".to_string(), "x".to_string()),
158
172
  ("TMUX".to_string(), "leader-pane".to_string()),
@@ -197,20 +211,27 @@ mod tests {
197
211
  "developer",
198
212
  Some("alpha"),
199
213
  );
200
- assert_eq!(env.get("TEAM_AGENT_ID").map(String::as_str), Some("developer"));
201
- assert_eq!(env.get("TEAM_AGENT_AGENT_ID").map(String::as_str), Some("developer"));
202
- assert_eq!(env.get("TEAM_AGENT_WORKSPACE").map(String::as_str), Some("/ws"));
203
- assert_eq!(env.get("TEAM_AGENT_OWNER_TEAM_ID").map(String::as_str), Some("alpha"));
214
+ assert_eq!(
215
+ env.get("TEAM_AGENT_ID").map(String::as_str),
216
+ Some("developer")
217
+ );
218
+ assert_eq!(
219
+ env.get("TEAM_AGENT_AGENT_ID").map(String::as_str),
220
+ Some("developer")
221
+ );
222
+ assert_eq!(
223
+ env.get("TEAM_AGENT_WORKSPACE").map(String::as_str),
224
+ Some("/ws")
225
+ );
226
+ assert_eq!(
227
+ env.get("TEAM_AGENT_OWNER_TEAM_ID").map(String::as_str),
228
+ Some("alpha")
229
+ );
204
230
  }
205
231
 
206
232
  #[test]
207
233
  fn worker_spawn_env_inherits_parent_env_minus_strip_list() {
208
- let env = worker_spawn_env(
209
- make_parent_env(&[]),
210
- Path::new("/ws"),
211
- "developer",
212
- None,
213
- );
234
+ let env = worker_spawn_env(make_parent_env(&[]), Path::new("/ws"), "developer", None);
214
235
  // Inherit-then-strip: every parent key passes through EXCEPT the
215
236
  // strip list. PATH-like + provider creds + random user vars all
216
237
  // present.
@@ -240,15 +261,24 @@ mod tests {
240
261
  #[test]
241
262
  fn worker_spawn_cwd_returns_workspace_when_no_spec_override() {
242
263
  // YAML agent with no spawn_cwd field.
243
- let agent = YamlValue::Map(vec![("id".to_string(), YamlValue::Str("developer".to_string()))]);
244
- assert_eq!(worker_spawn_cwd(&agent, Path::new("/ws")), PathBuf::from("/ws"));
264
+ let agent = YamlValue::Map(vec![(
265
+ "id".to_string(),
266
+ YamlValue::Str("developer".to_string()),
267
+ )]);
268
+ assert_eq!(
269
+ worker_spawn_cwd(&agent, Path::new("/ws")),
270
+ PathBuf::from("/ws")
271
+ );
245
272
  }
246
273
 
247
274
  #[test]
248
275
  fn worker_spawn_cwd_returns_yaml_override_when_present() {
249
276
  let agent = YamlValue::Map(vec![
250
277
  ("id".to_string(), YamlValue::Str("developer".to_string())),
251
- ("spawn_cwd".to_string(), YamlValue::Str("/explicit/cwd".to_string())),
278
+ (
279
+ "spawn_cwd".to_string(),
280
+ YamlValue::Str("/explicit/cwd".to_string()),
281
+ ),
252
282
  ]);
253
283
  assert_eq!(
254
284
  worker_spawn_cwd(&agent, Path::new("/ws")),
@@ -262,6 +292,9 @@ mod tests {
262
292
  ("id".to_string(), YamlValue::Str("developer".to_string())),
263
293
  ("spawn_cwd".to_string(), YamlValue::Str("".to_string())),
264
294
  ]);
265
- assert_eq!(worker_spawn_cwd(&agent, Path::new("/ws")), PathBuf::from("/ws"));
295
+ assert_eq!(
296
+ worker_spawn_cwd(&agent, Path::new("/ws")),
297
+ PathBuf::from("/ws")
298
+ );
266
299
  }
267
300
  }
@@ -63,7 +63,13 @@ pub(crate) fn resolve_workspace_for_hash(workspace: &Path) -> PathBuf {
63
63
  pub(crate) fn sanitize_session_folder(raw: &str) -> String {
64
64
  let replaced: String = raw
65
65
  .chars()
66
- .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.') { c } else { '_' })
66
+ .map(|c| {
67
+ if c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.') {
68
+ c
69
+ } else {
70
+ '_'
71
+ }
72
+ })
67
73
  .take(48)
68
74
  .collect();
69
75
  let trimmed = replaced.trim_matches(|c| matches!(c, '_' | '-' | '.'));
@@ -40,20 +40,38 @@ pub fn attach_leader(
40
40
  let targets = attach_leader_targets(workspace, &state);
41
41
  let pane_id = pane
42
42
  .cloned()
43
- .or_else(|| std::env::var("TMUX_PANE").ok().filter(|p| !p.is_empty()).map(PaneId::new))
43
+ .or_else(|| {
44
+ std::env::var("TMUX_PANE")
45
+ .ok()
46
+ .filter(|p| !p.is_empty())
47
+ .map(PaneId::new)
48
+ })
44
49
  .ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
45
50
  let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
46
51
  let Some(target) = targets.iter().find(|target| target.info.pane_id == pane_id) else {
47
- return Err(LeaderError::Validation(format!("tmux pane not found: {pane_id}")));
52
+ return Err(LeaderError::Validation(format!(
53
+ "tmux pane not found: {pane_id}"
54
+ )));
48
55
  };
49
- let mut receiver = receiver_for_attach_target(workspace, &state, &target.info, provider, Discovery::ExplicitPane)?;
56
+ let mut receiver = receiver_for_attach_target(
57
+ workspace,
58
+ &state,
59
+ &target.info,
60
+ provider,
61
+ Discovery::ExplicitPane,
62
+ )?;
50
63
  if let Some(endpoint) = target.endpoint.as_ref() {
51
64
  receiver.tmux_socket = Some(endpoint.clone());
52
65
  }
53
66
  let validation = validate_attach_target(workspace, &state, &target.info, provider);
54
67
  if validation.is_err() {
55
68
  let pane_info = pane_info_value(&target.info);
56
- let targets_value = Value::Array(targets.iter().map(|target| pane_info_value(&target.info)).collect());
69
+ let targets_value = Value::Array(
70
+ targets
71
+ .iter()
72
+ .map(|target| pane_info_value(&target.info))
73
+ .collect(),
74
+ );
57
75
  let owner_record = state_owner(&state);
58
76
  if let Some((readopted, validation)) = crate::leader::try_readopt_leader_pane(
59
77
  workspace,
@@ -66,8 +84,11 @@ pub fn attach_leader(
66
84
  LeaseSource::Manual,
67
85
  &event_log,
68
86
  )? {
69
- if let Some(endpoint) = target.endpoint.as_deref() { quiet_fake_leader_pane_echo(provider, &target.info, endpoint); }
70
- let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
87
+ if let Some(endpoint) = target.endpoint.as_deref() {
88
+ quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
89
+ }
90
+ let _ =
91
+ requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
71
92
  return Ok(LeaseResult {
72
93
  ok: true,
73
94
  status: LeaseStatus::Claimed,
@@ -90,7 +111,9 @@ pub fn attach_leader(
90
111
  "reason": validation.err().unwrap_or("leader_pane_validation_failed"),
91
112
  }),
92
113
  )?;
93
- return Err(LeaderError::Validation(format!("leader pane validation failed: {pane_id}")));
114
+ return Err(LeaderError::Validation(format!(
115
+ "leader pane validation failed: {pane_id}"
116
+ )));
94
117
  }
95
118
  let epoch = current_owner_epoch(&state);
96
119
  if state.get("team_owner").is_some() {
@@ -100,7 +123,9 @@ pub fn attach_leader(
100
123
  super::LeaderEvent::ReceiverAttached.name(),
101
124
  json!({"pane_id": pane_id.as_str(), "owner_epoch": epoch.0}),
102
125
  )?;
103
- if let Some(endpoint) = target.endpoint.as_deref() { quiet_fake_leader_pane_echo(provider, &target.info, endpoint); }
126
+ if let Some(endpoint) = target.endpoint.as_deref() {
127
+ quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
128
+ }
104
129
  let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
105
130
  return Ok(LeaseResult {
106
131
  ok: true,
@@ -125,7 +150,9 @@ pub fn attach_leader(
125
150
  super::LeaderEvent::ReceiverAttached.name(),
126
151
  json!({"pane_id": pane_id.as_str(), "owner_epoch": next_epoch.0}),
127
152
  )?;
128
- if let Some(endpoint) = target.endpoint.as_deref() { quiet_fake_leader_pane_echo(provider, &target.info, endpoint); }
153
+ if let Some(endpoint) = target.endpoint.as_deref() {
154
+ quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
155
+ }
129
156
  let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
130
157
  Ok(LeaseResult {
131
158
  ok: true,
@@ -279,7 +306,10 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
279
306
  .list_targets()
280
307
  .unwrap_or_default()
281
308
  .into_iter()
282
- .map(|info| AttachLeaderTarget { info, endpoint: None }),
309
+ .map(|info| AttachLeaderTarget {
310
+ info,
311
+ endpoint: None,
312
+ }),
283
313
  );
284
314
  targets
285
315
  }
@@ -336,11 +366,7 @@ fn requeue_exhausted_watchers_after_attach(
336
366
  let store = MessageStore::open(workspace)?;
337
367
  let team_id = TeamKey::new(crate::state::projection::team_state_key(state));
338
368
  let notices = crate::messaging::requeue_delivery_exhausted_watchers(
339
- workspace,
340
- &store,
341
- event_log,
342
- &team_id,
343
- pane_id,
369
+ workspace, &store, event_log, &team_id, pane_id,
344
370
  )?;
345
371
  event_log.write(
346
372
  super::LeaderEvent::ReceiverRequeuedExhaustedWatchers.name(),
@@ -359,7 +385,10 @@ pub(crate) fn requeued_exhausted_watchers_event_payload(
359
385
  _team_id: &TeamKey,
360
386
  notices: &[crate::messaging::WatcherNotice],
361
387
  ) -> serde_json::Value {
362
- let watcher_ids: Vec<&str> = notices.iter().map(|notice| notice.watcher_id.as_str()).collect();
388
+ let watcher_ids: Vec<&str> = notices
389
+ .iter()
390
+ .map(|notice| notice.watcher_id.as_str())
391
+ .collect();
363
392
  json!({
364
393
  "watcher_ids": watcher_ids,
365
394
  "count": watcher_ids.len(),
@@ -382,16 +411,32 @@ pub fn attach_leader_to_state(
382
411
  require_current: bool,
383
412
  ) -> Result<(LeaderReceiver, Value), LeaderError> {
384
413
  let _ = (source, require_current);
385
- let pane_id = pane.cloned().ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
414
+ let pane_id = pane
415
+ .cloned()
416
+ .ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
386
417
  let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
387
418
  let identity = leader_identity_context(workspace, None, Some(state))?;
388
419
  let epoch = current_owner_epoch(state);
389
- let receiver = make_receiver(provider, &non_empty_pane_id, &identity.leader_session_uuid, epoch, Discovery::EnvPane, None);
420
+ let receiver = make_receiver(
421
+ provider,
422
+ &non_empty_pane_id,
423
+ &identity.leader_session_uuid,
424
+ epoch,
425
+ Discovery::EnvPane,
426
+ None,
427
+ );
390
428
  if state.get("team_owner").is_some() {
391
429
  write_receiver_to_state(state, &receiver)?;
392
430
  } else {
393
431
  let next_epoch = OwnerEpoch(epoch.0.saturating_add(1));
394
- let receiver = make_receiver(provider, &non_empty_pane_id, &identity.leader_session_uuid, next_epoch, Discovery::EnvPane, None);
432
+ let receiver = make_receiver(
433
+ provider,
434
+ &non_empty_pane_id,
435
+ &identity.leader_session_uuid,
436
+ next_epoch,
437
+ Discovery::EnvPane,
438
+ None,
439
+ );
395
440
  let owner = make_owner(provider, &non_empty_pane_id, &identity, next_epoch);
396
441
  write_binding_to_state(state, &receiver, &owner)?;
397
442
  write_lease_dual_state(workspace, state)?;
@@ -436,7 +481,11 @@ pub fn claim_leader(
436
481
  let caller = std::env::var("TMUX_PANE")
437
482
  .ok()
438
483
  .filter(|pane| !pane.is_empty())
439
- .or_else(|| std::env::var("TEAM_AGENT_LEADER_PANE_ID").ok().filter(|pane| !pane.is_empty()))
484
+ .or_else(|| {
485
+ std::env::var("TEAM_AGENT_LEADER_PANE_ID")
486
+ .ok()
487
+ .filter(|pane| !pane.is_empty())
488
+ })
440
489
  .unwrap_or_default();
441
490
  let raw_state = crate::state::persist::load_runtime_state(workspace)?;
442
491
  let event_log = crate::event_log::EventLog::new(workspace);
@@ -458,13 +507,15 @@ pub fn claim_leader(
458
507
  let explicit_team = team.filter(|team| !team.is_empty());
459
508
  let requested_team = explicit_team
460
509
  .filter(|team| !team.is_empty())
461
- .or_else(|| caller_target.as_ref().and_then(|target| target.team_id.as_deref()))
510
+ .or_else(|| {
511
+ caller_target
512
+ .as_ref()
513
+ .and_then(|target| target.team_id.as_deref())
514
+ })
462
515
  .or(env_team.as_deref());
463
- let team_id = TeamKey::new(
464
- requested_team
465
- .map(str::to_string)
466
- .unwrap_or_else(|| crate::messaging::leader_receiver::active_team_key(workspace, &raw_state)),
467
- );
516
+ let team_id = TeamKey::new(requested_team.map(str::to_string).unwrap_or_else(|| {
517
+ crate::messaging::leader_receiver::active_team_key(workspace, &raw_state)
518
+ }));
468
519
  let active_team = crate::messaging::leader_receiver::active_team_key(workspace, &raw_state);
469
520
  let scoped_team = explicit_team.filter(|team| {
470
521
  *team == active_team
@@ -504,12 +555,7 @@ pub fn claim_leader(
504
555
  if let Some(pane) = result.bound_pane_id.as_ref() {
505
556
  let store = MessageStore::open(workspace)?;
506
557
  crate::messaging::watchers::requeue_after_claim_leader(
507
- workspace,
508
- &store,
509
- &event_log,
510
- &team_id,
511
- pane,
512
- None,
558
+ workspace, &store, &event_log, &team_id, pane, None,
513
559
  )?;
514
560
  }
515
561
  }
@@ -568,7 +614,9 @@ struct NonEmptyPaneId(PaneId);
568
614
  impl NonEmptyPaneId {
569
615
  fn try_from_pane(pane: &PaneId) -> Result<Self, LeaderError> {
570
616
  if pane.as_str().trim().is_empty() {
571
- return Err(LeaderError::Validation("leader pane id is empty".to_string()));
617
+ return Err(LeaderError::Validation(
618
+ "leader pane id is empty".to_string(),
619
+ ));
572
620
  }
573
621
  Ok(Self(pane.clone()))
574
622
  }
@@ -640,14 +688,13 @@ fn claim_lease_no_incident_with_target(
640
688
  } else {
641
689
  None
642
690
  };
643
- let (mut topology_convergence, converged) =
644
- apply_endpoint_convergence(
645
- state,
646
- team_id,
647
- convergence_candidate,
648
- candidate_source,
649
- pre_epoch,
650
- );
691
+ let (mut topology_convergence, converged) = apply_endpoint_convergence(
692
+ state,
693
+ team_id,
694
+ convergence_candidate,
695
+ candidate_source,
696
+ pre_epoch,
697
+ );
651
698
  if converged {
652
699
  write_claim_state(workspace, state, scoped_team, team)?;
653
700
  topology_convergence = verify_persisted_topology_convergence(
@@ -681,14 +728,12 @@ fn claim_lease_no_incident_with_target(
681
728
  topology_convergence,
682
729
  });
683
730
  }
684
- let owner_live = bound_pane_id
685
- .as_deref()
686
- .is_some_and(|pane| {
687
- if pane == caller_pane.as_str() && !bound_endpoint_matches_caller {
688
- return false;
689
- }
690
- liveness.liveness(pane) == PaneLiveness::Live
691
- });
731
+ let owner_live = bound_pane_id.as_deref().is_some_and(|pane| {
732
+ if pane == caller_pane.as_str() && !bound_endpoint_matches_caller {
733
+ return false;
734
+ }
735
+ liveness.liveness(pane) == PaneLiveness::Live
736
+ });
692
737
  if owner_live && !confirm {
693
738
  emit_lease_refusal(
694
739
  event_log,
@@ -724,9 +769,9 @@ fn claim_lease_no_incident_with_target(
724
769
  ));
725
770
  }
726
771
  let locked_bound_pane = bound_pane(&locked);
727
- let locked_owner_live = locked_bound_pane
728
- .as_deref()
729
- .is_some_and(|pane| pane != caller_pane.as_str() && liveness.liveness(pane) == PaneLiveness::Live);
772
+ let locked_owner_live = locked_bound_pane.as_deref().is_some_and(|pane| {
773
+ pane != caller_pane.as_str() && liveness.liveness(pane) == PaneLiveness::Live
774
+ });
730
775
  if locked_owner_live && !confirm {
731
776
  emit_lease_refusal(
732
777
  event_log,
@@ -750,10 +795,8 @@ fn claim_lease_no_incident_with_target(
750
795
  // pane → delivery routes worker messages to itself (loop) and the leader
751
796
  // loses its handle (the macmini "hand-handle mapping 灾难" truth source).
752
797
  if let Some(caller_pane_info) = caller_pane_info {
753
- match crate::topology::classify_registered_worker_for_observed_pane(
754
- state,
755
- caller_pane_info,
756
- ) {
798
+ match crate::topology::classify_registered_worker_for_observed_pane(state, caller_pane_info)
799
+ {
757
800
  crate::topology::WorkerPaneBindingMatch::LiveSameWorker { agent_id } => {
758
801
  emit_lease_refusal(
759
802
  event_log,
@@ -829,14 +872,13 @@ fn claim_lease_no_incident_with_target(
829
872
  } else {
830
873
  None
831
874
  };
832
- let (mut topology_convergence, converged) =
833
- apply_endpoint_convergence(
834
- state,
835
- team_id,
836
- receiver.tmux_socket.as_deref(),
837
- candidate_source,
838
- next_epoch,
839
- );
875
+ let (mut topology_convergence, converged) = apply_endpoint_convergence(
876
+ state,
877
+ team_id,
878
+ receiver.tmux_socket.as_deref(),
879
+ candidate_source,
880
+ next_epoch,
881
+ );
840
882
  write_claim_state(workspace, state, scoped_team, team)?;
841
883
  if converged {
842
884
  topology_convergence = verify_persisted_topology_convergence(
@@ -858,7 +900,12 @@ fn claim_lease_no_incident_with_target(
858
900
  ));
859
901
  }
860
902
  }
861
- let uuid_prefix = identity.leader_session_uuid.as_str().chars().take(8).collect::<String>();
903
+ let uuid_prefix = identity
904
+ .leader_session_uuid
905
+ .as_str()
906
+ .chars()
907
+ .take(8)
908
+ .collect::<String>();
862
909
  if reason == LeaseReason::PreviousOwnerPaneDead {
863
910
  event_log.write(
864
911
  super::LeaderEvent::OwnerAdoptedOnRestart.name(),
@@ -1105,7 +1152,10 @@ fn mark_convergence_persisted(metadata: &mut Value, team_id: &str) {
1105
1152
  return;
1106
1153
  };
1107
1154
  obj.insert("persisted".to_string(), json!(true));
1108
- obj.insert("checked_paths".to_string(), json!(convergence_checked_paths(team_id)));
1155
+ obj.insert(
1156
+ "checked_paths".to_string(),
1157
+ json!(convergence_checked_paths(team_id)),
1158
+ );
1109
1159
  }
1110
1160
 
1111
1161
  fn mark_convergence_persistence_conflict(metadata: &mut Value, team_id: &str) {
@@ -1114,7 +1164,10 @@ fn mark_convergence_persistence_conflict(metadata: &mut Value, team_id: &str) {
1114
1164
  };
1115
1165
  obj.insert("status".to_string(), json!("persistence_conflict"));
1116
1166
  obj.insert("persisted".to_string(), json!(false));
1117
- obj.insert("checked_paths".to_string(), json!(convergence_checked_paths(team_id)));
1167
+ obj.insert(
1168
+ "checked_paths".to_string(),
1169
+ json!(convergence_checked_paths(team_id)),
1170
+ );
1118
1171
  obj.insert(
1119
1172
  "action".to_string(),
1120
1173
  json!("endpoint convergence was not durably persisted; retry claim-leader or takeover"),
@@ -1170,7 +1223,11 @@ fn refused(
1170
1223
  owner: None,
1171
1224
  owner_epoch: epoch,
1172
1225
  reason: Some(reason),
1173
- action: if action.is_empty() { None } else { Some(action.to_string()) },
1226
+ action: if action.is_empty() {
1227
+ None
1228
+ } else {
1229
+ Some(action.to_string())
1230
+ },
1174
1231
  bound_pane_id,
1175
1232
  topology_convergence: None,
1176
1233
  }
@@ -1178,7 +1235,8 @@ fn refused(
1178
1235
 
1179
1236
  fn current_owner_epoch(state: &Value) -> OwnerEpoch {
1180
1237
  let owner_epoch = get_path_u64(state, &["team_owner", "owner_epoch"]).filter(|v| *v != 0);
1181
- let receiver_epoch = get_path_u64(state, &["leader_receiver", "owner_epoch"]).filter(|v| *v != 0);
1238
+ let receiver_epoch =
1239
+ get_path_u64(state, &["leader_receiver", "owner_epoch"]).filter(|v| *v != 0);
1182
1240
  OwnerEpoch(owner_epoch.or(receiver_epoch).unwrap_or(0))
1183
1241
  }
1184
1242
 
@@ -1189,7 +1247,9 @@ fn bound_pane(state: &Value) -> Option<String> {
1189
1247
  }
1190
1248
 
1191
1249
  fn bound_endpoint_matches_current_process(state: &Value) -> bool {
1192
- let Some(bound) = get_path_str(state, &["leader_receiver", "tmux_socket"]).filter(|v| !v.is_empty()) else {
1250
+ let Some(bound) =
1251
+ get_path_str(state, &["leader_receiver", "tmux_socket"]).filter(|v| !v.is_empty())
1252
+ else {
1193
1253
  return true;
1194
1254
  };
1195
1255
  let Some(current) = crate::tmux_backend::socket_name_from_tmux_env() else {
@@ -1316,7 +1376,11 @@ fn claim_target_from_pane_info(workspace: &Path, target: &PaneInfo) -> Option<Le
1316
1376
  Some(LeaderClaimTarget {
1317
1377
  provider,
1318
1378
  leader_session_uuid: target_leader_session_uuid(target),
1319
- team_id: target.leader_env.get("TEAM_AGENT_TEAM_ID").filter(|raw| !raw.is_empty()).cloned(),
1379
+ team_id: target
1380
+ .leader_env
1381
+ .get("TEAM_AGENT_TEAM_ID")
1382
+ .filter(|raw| !raw.is_empty())
1383
+ .cloned(),
1320
1384
  pane_info: Some(target.clone()),
1321
1385
  endpoint: None,
1322
1386
  })
@@ -1359,7 +1423,10 @@ fn validate_attach_target(
1359
1423
  .or_else(|| get_path_str(state, &["leader_receiver", "leader_session_uuid"]));
1360
1424
  if let (Some(recorded), Some(target_uuid)) = (
1361
1425
  recorded_uuid.as_deref(),
1362
- claim_target.leader_session_uuid.as_ref().map(|u| u.as_str()),
1426
+ claim_target
1427
+ .leader_session_uuid
1428
+ .as_ref()
1429
+ .map(|u| u.as_str()),
1363
1430
  ) {
1364
1431
  if recorded != target_uuid {
1365
1432
  return Err("leader_session_uuid_mismatch");
@@ -1472,7 +1539,10 @@ impl crate::state::owner_gate::PaneLivenessProbe for TargetScanLiveness {
1472
1539
  }
1473
1540
  }
1474
1541
 
1475
- fn locked_runtime_state(workspace: &Path, scoped_team: Option<&str>) -> Result<Option<Value>, LeaderError> {
1542
+ fn locked_runtime_state(
1543
+ workspace: &Path,
1544
+ scoped_team: Option<&str>,
1545
+ ) -> Result<Option<Value>, LeaderError> {
1476
1546
  let path = crate::state::persist::runtime_state_path(workspace);
1477
1547
  if !path.exists() {
1478
1548
  return Ok(None);
@@ -1537,9 +1607,13 @@ fn make_receiver(
1537
1607
  provider,
1538
1608
  pane_id: pane.as_pane_id().clone(),
1539
1609
  session_name: target.as_ref().map(|t| t.session.clone()),
1540
- window_index: target.as_ref().and_then(|t| t.window_index.map(|v| v.to_string())),
1610
+ window_index: target
1611
+ .as_ref()
1612
+ .and_then(|t| t.window_index.map(|v| v.to_string())),
1541
1613
  window_name: target.as_ref().and_then(|t| t.window_name.clone()),
1542
- pane_index: target.as_ref().and_then(|t| t.pane_index.map(|v| v.to_string())),
1614
+ pane_index: target
1615
+ .as_ref()
1616
+ .and_then(|t| t.pane_index.map(|v| v.to_string())),
1543
1617
  pane_tty: target.as_ref().and_then(|t| t.tty.clone()),
1544
1618
  pane_current_command: target.as_ref().and_then(|t| t.current_command.clone()),
1545
1619
  tmux_socket: crate::tmux_backend::socket_name_from_tmux_env(),
@@ -1557,8 +1631,12 @@ fn receiver_fingerprint(target: &PaneInfo) -> String {
1557
1631
  format!(
1558
1632
  "{}|{}|{}|{}",
1559
1633
  target.session.as_str(),
1560
- target.window_index.map_or_else(String::new, |v| v.to_string()),
1561
- target.pane_index.map_or_else(String::new, |v| v.to_string()),
1634
+ target
1635
+ .window_index
1636
+ .map_or_else(String::new, |v| v.to_string()),
1637
+ target
1638
+ .pane_index
1639
+ .map_or_else(String::new, |v| v.to_string()),
1562
1640
  target.tty.as_deref().unwrap_or("")
1563
1641
  )
1564
1642
  }
@@ -1605,7 +1683,9 @@ fn write_binding_to_state(
1605
1683
  *state = json!({});
1606
1684
  }
1607
1685
  if !state.is_object() {
1608
- return Err(LeaderError::Validation("state root is not an object".to_string()));
1686
+ return Err(LeaderError::Validation(
1687
+ "state root is not an object".to_string(),
1688
+ ));
1609
1689
  }
1610
1690
  let team_key = canonical_owner_write_key(state);
1611
1691
  let record = crate::state::ownership::OwnershipWrite::new()
@@ -1624,7 +1704,9 @@ fn write_receiver_to_state(
1624
1704
  *state = json!({});
1625
1705
  }
1626
1706
  if !state.is_object() {
1627
- return Err(LeaderError::Validation("state root is not an object".to_string()));
1707
+ return Err(LeaderError::Validation(
1708
+ "state root is not an object".to_string(),
1709
+ ));
1628
1710
  }
1629
1711
  let team_key = canonical_owner_write_key(state);
1630
1712
  let record = crate::state::ownership::OwnershipWrite::new()
@@ -1750,7 +1832,25 @@ fn state_owner(state: &Value) -> Option<TeamOwner> {
1750
1832
  /// name for 0.5.x call-site stability. The B0 legacy snapshot is
1751
1833
  /// diagnostic-only via `lifecycle::save_team_runtime_snapshot`.
1752
1834
  pub fn write_lease_dual_state(workspace: &Path, state: &Value) -> Result<(), LeaderError> {
1753
- crate::state::persist::save_runtime_state(workspace, state)?;
1835
+ // 0.5.42 S1b (s1b-writer-cluster-locate.md §4.4): terminal root
1836
+ // save routes through `StateRepository` with the `ClaimLeader`
1837
+ // intent. Dispatch is byte-identical to the old
1838
+ // `save_runtime_state` helper (see
1839
+ // `state/repository.rs:ClaimLeader => helper_write_root`), so the
1840
+ // full lease writer cluster (attach_leader / attach_app_server /
1841
+ // attach_leader_to_state / claim_lease_no_incident_with_target /
1842
+ // rediscover) keeps its persist/merge/CAS/readback semantics.
1843
+ // Public signature/error mapping preserved for caller stability.
1844
+ // Repository does NOT compute owner_epoch, re-run convergence,
1845
+ // emit ownership events, or acquire a second lease lock — the
1846
+ // caller still owns all of that.
1847
+ let team_key = canonical_owner_write_key(state);
1848
+ crate::state::repository::StateRepository::new(workspace).save(
1849
+ crate::state::repository::StateWriteIntent::ClaimLeader {
1850
+ team_key: team_key.as_str(),
1851
+ },
1852
+ state,
1853
+ )?;
1754
1854
  Ok(())
1755
1855
  }
1756
1856
 
@@ -1769,7 +1869,11 @@ fn write_claim_state(
1769
1869
  }
1770
1870
  }
1771
1871
 
1772
- fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &str) -> Result<(), LeaderError> {
1872
+ fn save_claim_team_scoped_state(
1873
+ workspace: &Path,
1874
+ state: &Value,
1875
+ target_key: &str,
1876
+ ) -> Result<(), LeaderError> {
1773
1877
  let existing = crate::state::persist::load_runtime_state(workspace)?;
1774
1878
  let mut teams = existing
1775
1879
  .get("teams")
@@ -1838,7 +1942,16 @@ fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &st
1838
1942
  writes_endpoint_convergence,
1839
1943
  );
1840
1944
  merged.insert("teams".to_string(), Value::Object(teams));
1841
- crate::state::persist::save_runtime_state(workspace, &Value::Object(merged))?;
1945
+ // 0.5.42 S1b (s1b-writer-cluster-locate.md §4.4): only the
1946
+ // terminal save routes through `StateRepository`; the entire
1947
+ // load/merge/compact block above stays in place — repository is a
1948
+ // writer facade, not a projection assembler.
1949
+ crate::state::repository::StateRepository::new(workspace).save(
1950
+ crate::state::repository::StateWriteIntent::ClaimLeader {
1951
+ team_key: target_key,
1952
+ },
1953
+ &Value::Object(merged),
1954
+ )?;
1842
1955
  Ok(())
1843
1956
  }
1844
1957