@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
@@ -207,7 +207,10 @@ mod tests {
207
207
  t("a", &["b"], TaskStatus::Pending),
208
208
  t("b", &["a"], TaskStatus::Pending),
209
209
  ];
210
- assert_eq!(cycle_ids(&find_dependency_cycle(&tasks)), vec!["a", "b", "a"]);
210
+ assert_eq!(
211
+ cycle_ids(&find_dependency_cycle(&tasks)),
212
+ vec!["a", "b", "a"]
213
+ );
211
214
  }
212
215
 
213
216
  #[test]
@@ -239,7 +242,10 @@ mod tests {
239
242
  t("a", &["b"], TaskStatus::Pending),
240
243
  t("b", &["a"], TaskStatus::Pending),
241
244
  ];
242
- assert_eq!(cycle_ids(&find_dependency_cycle(&tasks)), vec!["a", "b", "a"]);
245
+ assert_eq!(
246
+ cycle_ids(&find_dependency_cycle(&tasks)),
247
+ vec!["a", "b", "a"]
248
+ );
243
249
  }
244
250
 
245
251
  #[test]
@@ -351,7 +357,14 @@ mod tests {
351
357
  let mut node = t("a", &[], TaskStatus::Pending);
352
358
  node.last_result_summary = Some("old".to_string());
353
359
  let mut tasks = [node];
354
- update_task_status(&mut tasks, &TaskId::new("a"), TaskStatus::Running, None, None).unwrap();
360
+ update_task_status(
361
+ &mut tasks,
362
+ &TaskId::new("a"),
363
+ TaskStatus::Running,
364
+ None,
365
+ None,
366
+ )
367
+ .unwrap();
355
368
  assert_eq!(tasks[0].status, TaskStatus::Running);
356
369
  assert_eq!(tasks[0].last_result_summary.as_deref(), Some("old"));
357
370
  }
@@ -360,9 +373,14 @@ mod tests {
360
373
  fn update_unknown_id_errors() {
361
374
  // Python U4:未知 id -> KeyError "Unknown task id: zzz"。Rust -> Runtime。
362
375
  let mut tasks = [t("a", &[], TaskStatus::Pending)];
363
- let err =
364
- update_task_status(&mut tasks, &TaskId::new("zzz"), TaskStatus::Done, None, None)
365
- .unwrap_err();
376
+ let err = update_task_status(
377
+ &mut tasks,
378
+ &TaskId::new("zzz"),
379
+ TaskStatus::Done,
380
+ None,
381
+ None,
382
+ )
383
+ .unwrap_err();
366
384
  assert_eq!(err, ModelError::Runtime("Unknown task id: zzz".to_string()));
367
385
  }
368
386
 
@@ -371,10 +389,19 @@ mod tests {
371
389
  // Python U5:summary=None 只写 artifact_refs
372
390
  let mut tasks = [t("a", &[], TaskStatus::Pending)];
373
391
  let refs = vec![serde_json::json!({"r": 1})];
374
- update_task_status(&mut tasks, &TaskId::new("a"), TaskStatus::Done, None, Some(refs))
375
- .unwrap();
392
+ update_task_status(
393
+ &mut tasks,
394
+ &TaskId::new("a"),
395
+ TaskStatus::Done,
396
+ None,
397
+ Some(refs),
398
+ )
399
+ .unwrap();
376
400
  assert_eq!(tasks[0].status, TaskStatus::Done);
377
401
  assert!(tasks[0].last_result_summary.is_none());
378
- assert_eq!(tasks[0].artifact_refs, Some(vec![serde_json::json!({"r": 1})]));
402
+ assert_eq!(
403
+ tasks[0].artifact_refs,
404
+ Some(vec![serde_json::json!({"r": 1})])
405
+ );
379
406
  }
380
407
  }
@@ -53,7 +53,10 @@ fn skill_front_matter_roundtrip() {
53
53
 
54
54
  #[test]
55
55
  fn block_scalar_basic() {
56
- roundtrip_eq("msg: |\n line one\n line two\n", "msg: |\n line one\n line two\n");
56
+ roundtrip_eq(
57
+ "msg: |\n line one\n line two\n",
58
+ "msg: |\n line one\n line two\n",
59
+ );
57
60
  }
58
61
 
59
62
  #[test]
@@ -86,12 +89,18 @@ fn int_plus_and_underscore() {
86
89
 
87
90
  #[test]
88
91
  fn non_ints_stay_strings() {
89
- roundtrip_eq("a: 1.5\nb: 0x1f\nc: 1e3\nd: _1\n", "a: \"1.5\"\nb: \"0x1f\"\nc: \"1e3\"\nd: \"_1\"\n");
92
+ roundtrip_eq(
93
+ "a: 1.5\nb: 0x1f\nc: 1e3\nd: _1\n",
94
+ "a: \"1.5\"\nb: \"0x1f\"\nc: \"1e3\"\nd: \"_1\"\n",
95
+ );
90
96
  }
91
97
 
92
98
  #[test]
93
99
  fn bools_and_nulls() {
94
- roundtrip_eq("a: true\nb: False\nc: NULL\nd: ~\n", "a: true\nb: false\nc: null\nd: null\n");
100
+ roundtrip_eq(
101
+ "a: true\nb: False\nc: NULL\nd: ~\n",
102
+ "a: true\nb: false\nc: null\nd: null\n",
103
+ );
95
104
  }
96
105
 
97
106
  #[test]
@@ -119,7 +128,10 @@ fn list_of_maps() {
119
128
 
120
129
  #[test]
121
130
  fn scalar_list_mixed() {
122
- roundtrip_eq("a:\n - 1\n - two\n - true\n", "a:\n - 1\n - \"two\"\n - true\n");
131
+ roundtrip_eq(
132
+ "a:\n - 1\n - two\n - true\n",
133
+ "a:\n - 1\n - \"two\"\n - true\n",
134
+ );
123
135
  }
124
136
 
125
137
  #[test]
@@ -130,7 +142,10 @@ fn empty_value_is_null() {
130
142
  #[test]
131
143
  fn inline_comment_not_stripped() {
132
144
  // 整行注释被跳过;行内 `#` 不被当注释,留在值里。
133
- roundtrip_eq("# top\na: 1 # inline?\nb: 2\n", "a: \"1 # inline?\"\nb: 2\n");
145
+ roundtrip_eq(
146
+ "# top\na: 1 # inline?\nb: 2\n",
147
+ "a: \"1 # inline?\"\nb: 2\n",
148
+ );
134
149
  }
135
150
 
136
151
  #[test]
@@ -213,7 +228,10 @@ fn dump_mixed_list() {
213
228
  Value::Map(Vec::new()),
214
229
  Value::List(Vec::new()),
215
230
  ]);
216
- assert_eq!(dumps(&v), "- 1\n- \"two\"\n-\n - 3\n - 4\n- k: \"v\"\n- {}\n-\n");
231
+ assert_eq!(
232
+ dumps(&v),
233
+ "- 1\n- \"two\"\n-\n - 3\n - 4\n- k: \"v\"\n- {}\n-\n"
234
+ );
217
235
  }
218
236
 
219
237
  #[test]
@@ -86,7 +86,10 @@ impl Value {
86
86
  }
87
87
  /// dict get:首个匹配 key 的值(`insert_ordered` 已去重 → 首即唯一)。非 Map → `None`。
88
88
  pub fn get(&self, key: &str) -> Option<&Value> {
89
- self.as_map()?.iter().find(|(k, _)| k == key).map(|(_, v)| v)
89
+ self.as_map()?
90
+ .iter()
91
+ .find(|(k, _)| k == key)
92
+ .map(|(_, v)| v)
90
93
  }
91
94
  /// Python 真值语义:None/Null/false/0/""/空集 → false。
92
95
  pub fn is_truthy(&self) -> bool {
@@ -330,7 +333,8 @@ fn parse_scalar(raw: &str) -> Value {
330
333
  if raw == "{}" {
331
334
  return Value::Map(Vec::new());
332
335
  }
333
- if (raw.starts_with('"') && raw.ends_with('"')) || (raw.starts_with('\'') && raw.ends_with('\''))
336
+ if (raw.starts_with('"') && raw.ends_with('"'))
337
+ || (raw.starts_with('\'') && raw.ends_with('\''))
334
338
  {
335
339
  if raw.len() < 2 {
336
340
  // 单个引号字符:不是合法的成对引号,落到末尾 return raw。
@@ -576,10 +580,7 @@ fn dump(value: &Value, indent: usize) -> Vec<String> {
576
580
  lines.extend(dump(child, indent + 4));
577
581
  } else {
578
582
  // 注意:此分支**不**对多行字符串做 `|`,与 Python 一致。
579
- lines.push(format!(
580
- "{pad}{prefix}{key}: {}",
581
- format_scalar(child)
582
- ));
583
+ lines.push(format!("{pad}{prefix}{key}: {}", format_scalar(child)));
583
584
  }
584
585
  first = false;
585
586
  }
@@ -105,7 +105,9 @@ pub fn uninstall(opts: &UninstallOptions) -> Result<UninstallOutcome, PackagingE
105
105
  /// `--target all` fan-out 两者;`--dest` 不能与 `--target all` 组合(`commands.py:453` → Err)。
106
106
  /// 拷前清陈旧残留(修 `dirs_exist_ok` 残留);`--dry-run` 只报告不落地。
107
107
  /// // REAL-MACHINE-E2E:真拷 / removed_stale 需文件系统;dry-run 与 plan 可单测。
108
- pub fn install_skill(opts: &SkillInstallOptions) -> Result<Vec<SkillInstallOutcome>, PackagingError> {
108
+ pub fn install_skill(
109
+ opts: &SkillInstallOptions,
110
+ ) -> Result<Vec<SkillInstallOutcome>, PackagingError> {
109
111
  if opts.target == SkillTarget::All && opts.dest.is_some() {
110
112
  return Err(PackagingError::InvalidOptions(
111
113
  "--dest cannot be combined with --target all".to_string(),
@@ -120,9 +122,9 @@ pub fn install_skill(opts: &SkillInstallOptions) -> Result<Vec<SkillInstallOutco
120
122
  for target in targets {
121
123
  let dest = match &opts.dest {
122
124
  Some(dest) => SkillDestDir(dest.clone()),
123
- None => target
124
- .dest_dir(&home)
125
- .ok_or_else(|| PackagingError::InvalidOptions("target all has no single dest".to_string()))?,
125
+ None => target.dest_dir(&home).ok_or_else(|| {
126
+ PackagingError::InvalidOptions("target all has no single dest".to_string())
127
+ })?,
126
128
  };
127
129
  let mut removed_stale = Vec::new();
128
130
  if !opts.dry_run {
@@ -169,7 +171,9 @@ pub fn diagnose_path(bin_dir: &BinDir) -> Result<PathHint, PackagingError> {
169
171
  .map(PathBuf::from)
170
172
  .collect();
171
173
  if entries.iter().any(|p| p == &bin_dir.0) {
172
- return Ok(PathHint::OnPath { bin_dir: bin_dir.0.clone() });
174
+ return Ok(PathHint::OnPath {
175
+ bin_dir: bin_dir.0.clone(),
176
+ });
173
177
  }
174
178
  let executable_bit_set = bin_dir.0.join("team-agent").metadata().is_ok_and(|m| {
175
179
  #[cfg(unix)]
@@ -197,14 +201,21 @@ pub fn diagnose_path(bin_dir: &BinDir) -> Result<PathHint, PackagingError> {
197
201
  }
198
202
 
199
203
  fn home_dir() -> PathBuf {
200
- std::env::var_os("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."))
204
+ std::env::var_os("HOME")
205
+ .map(PathBuf::from)
206
+ .unwrap_or_else(|| PathBuf::from("."))
201
207
  }
202
208
 
203
209
  fn default_skill_source() -> PathBuf {
204
- PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("skills").join("team-agent")
210
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
211
+ .join("skills")
212
+ .join("team-agent")
205
213
  }
206
214
 
207
- fn atomic_replace_binary(source: &Path, dest: &Path) -> Result<AtomicReplaceOutcome, PackagingError> {
215
+ fn atomic_replace_binary(
216
+ source: &Path,
217
+ dest: &Path,
218
+ ) -> Result<AtomicReplaceOutcome, PackagingError> {
208
219
  if !source.exists() {
209
220
  return Err(PackagingError::Io(std::io::Error::new(
210
221
  std::io::ErrorKind::NotFound,
@@ -293,7 +304,10 @@ fn staging_dir_for(dest: &Path) -> Result<PathBuf, PackagingError> {
293
304
  .file_name()
294
305
  .and_then(|name| name.to_str())
295
306
  .unwrap_or("skill");
296
- let parent = dest.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."));
307
+ let parent = dest
308
+ .parent()
309
+ .map(Path::to_path_buf)
310
+ .unwrap_or_else(|| PathBuf::from("."));
297
311
  std::fs::create_dir_all(&parent)?;
298
312
  Ok(parent.join(format!(".{name}.ta-staging-{}", std::process::id())))
299
313
  }
@@ -11,14 +11,12 @@ use crate::db::migration::schema_diagnosis_workspace;
11
11
  /// **§84**:只调 step 3/11/12 的 trait 入口,注入 mock 时 provider 调用计数 = 0;绝不触发 prompt/token。
12
12
  pub fn doctor(opts: &DoctorOptions) -> Result<DoctorStatus, PackagingError> {
13
13
  if opts.fix && opts.gate.is_none() {
14
- return Err(PackagingError::InvalidOptions("--fix requires --gate".to_string()));
14
+ return Err(PackagingError::InvalidOptions(
15
+ "--fix requires --gate".to_string(),
16
+ ));
15
17
  }
16
- let gate_blockers = crate::diagnose::doctor_gate_blockers(
17
- &opts.workspace,
18
- opts.gate,
19
- opts.fix,
20
- opts.confirm,
21
- )?;
18
+ let gate_blockers =
19
+ crate::diagnose::doctor_gate_blockers(&opts.workspace, opts.gate, opts.fix, opts.confirm)?;
22
20
  if !gate_blockers.is_empty() {
23
21
  return Ok(DoctorStatus::HasBlockers {
24
22
  blockers: gate_blockers,
@@ -53,7 +53,15 @@
53
53
  //! 统一加(原子替换/migration·repair 段建议局部等同 daemon 门),本骨架不加。
54
54
 
55
55
  // ROUND-0 skeleton:fn body 全 unimplemented!() → import/field/method 暂未被用;P2 porter 落实现时移除。
56
- #![allow(dead_code, unused_imports, unused_variables, clippy::result_large_err, clippy::doc_overindented_list_items, clippy::doc_lazy_continuation, clippy::io_other_error)]
56
+ #![allow(
57
+ dead_code,
58
+ unused_imports,
59
+ unused_variables,
60
+ clippy::result_large_err,
61
+ clippy::doc_overindented_list_items,
62
+ clippy::doc_lazy_continuation,
63
+ clippy::io_other_error
64
+ )]
57
65
  // §10:原子替换二进制 / migration·repair 路径实现层禁 unwrap/expect/panic(unimplemented!() stub 不被拦);
58
66
  // tests 子模块各自 allow。
59
67
  #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
@@ -14,11 +14,18 @@ pub fn repair_schema(workspace: &Path) -> Result<MigrationOutcome, PackagingErro
14
14
  match fix_schema_layout(workspace, SCHEMA_VERSION)? {
15
15
  FixResult::Missing(diagnosis) => Ok(MigrationOutcome::UpToDate { diagnosis }),
16
16
  FixResult::Blocked { reason } => Ok(MigrationOutcome::Blocked { reason }),
17
- FixResult::Fixed { diagnosis, rebuilds } if rebuilds.is_empty() => {
18
- Ok(MigrationOutcome::UpToDate { diagnosis })
19
- }
20
- FixResult::Fixed { diagnosis, rebuilds } => {
21
- Ok(MigrationOutcome::Migrated { fix: FixResult::Fixed { diagnosis, rebuilds } })
22
- }
17
+ FixResult::Fixed {
18
+ diagnosis,
19
+ rebuilds,
20
+ } if rebuilds.is_empty() => Ok(MigrationOutcome::UpToDate { diagnosis }),
21
+ FixResult::Fixed {
22
+ diagnosis,
23
+ rebuilds,
24
+ } => Ok(MigrationOutcome::Migrated {
25
+ fix: FixResult::Fixed {
26
+ diagnosis,
27
+ rebuilds,
28
+ },
29
+ }),
23
30
  }
24
31
  }
@@ -130,9 +130,21 @@ fn version_current_is_not_a_hand_copied_python_drift_literal() {
130
130
  env!("CARGO_PKG_VERSION"),
131
131
  "single source of truth = CARGO_PKG_VERSION"
132
132
  );
133
- assert_ne!(v.as_str(), "0.1.4", "must not hand-copy pyproject.toml drift source");
134
- assert_ne!(v.as_str(), "0.2.11", "must not hand-copy package.json drift source");
135
- assert_ne!(v.as_str(), "dev", "must not be install.mjs:54 'dev' fallback");
133
+ assert_ne!(
134
+ v.as_str(),
135
+ "0.1.4",
136
+ "must not hand-copy pyproject.toml drift source"
137
+ );
138
+ assert_ne!(
139
+ v.as_str(),
140
+ "0.2.11",
141
+ "must not hand-copy package.json drift source"
142
+ );
143
+ assert_ne!(
144
+ v.as_str(),
145
+ "dev",
146
+ "must not be install.mjs:54 'dev' fallback"
147
+ );
136
148
  }
137
149
 
138
150
  #[test]
@@ -143,8 +155,11 @@ fn no_literal_version_string_hardcoded_in_packaging_code() {
143
155
  // 0.1.4 / 0.2.11 legitimately appear in doc/line comments documenting the bug, so we strip
144
156
  // comment text first and scan only executable code. This is the one place where
145
157
  // "double-source-drift forbidden" is statically checked against the source itself.
146
- let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/packaging/types.rs"))
147
- .expect("read own source");
158
+ let src = std::fs::read_to_string(concat!(
159
+ env!("CARGO_MANIFEST_DIR"),
160
+ "/src/packaging/types.rs"
161
+ ))
162
+ .expect("read own source");
148
163
  // Production region only (the #[cfg(test)] mod uses these literals as golden anti-examples):
149
164
  let prod = match src.find("#[cfg(test)]") {
150
165
  Some(i) => &src[..i],
@@ -375,7 +390,8 @@ fn doctor_drifted_db_emits_schema_layout_drift_blocker() {
375
390
  // "team.db physical layout drift detected" (EXACT string, commands.py:248).
376
391
  let ws = seed_workspace_with_drifted_db("blocker");
377
392
  let opts = doctor_opts(&ws);
378
- let status = doctor(&opts).expect("doctor on drifted workspace should succeed (returns blockers)");
393
+ let status =
394
+ doctor(&opts).expect("doctor on drifted workspace should succeed (returns blockers)");
379
395
  match status {
380
396
  DoctorStatus::HasBlockers { blockers } => {
381
397
  let drift = blockers
@@ -410,7 +426,10 @@ fn doctor_status_has_blockers_carries_typed_source() {
410
426
  };
411
427
  let json = serde_json::to_string(&status).unwrap();
412
428
  assert!(json.contains("\"status\":\"has_blockers\""), "got: {json}");
413
- assert!(json.contains("\"source\":\"schema_layout_drift\""), "got: {json}");
429
+ assert!(
430
+ json.contains("\"source\":\"schema_layout_drift\""),
431
+ "got: {json}"
432
+ );
414
433
  // detail 精确 == commands.py:248 schema_error 文本.
415
434
  assert!(
416
435
  json.contains("team.db physical layout drift detected"),
@@ -508,7 +527,10 @@ fn diagnose_path_not_on_path_npmrc_prefix_is_none_no_npm() {
508
527
  let bin = BinDir(PathBuf::from("/zzz-definitely-not-on-path-9f3a"));
509
528
  let hint = diagnose_path(&bin).expect("diagnose off-path bin");
510
529
  match hint {
511
- PathHint::NotOnPath { bin_dir, diagnostic } => {
530
+ PathHint::NotOnPath {
531
+ bin_dir,
532
+ diagnostic,
533
+ } => {
512
534
  assert_eq!(bin_dir, PathBuf::from("/zzz-definitely-not-on-path-9f3a"));
513
535
  // Rust 无 npm → 绝不重新引入 .npmrc 解析.
514
536
  assert_eq!(diagnostic.npmrc_prefix, None);
@@ -556,11 +578,7 @@ fn skill_opts(target: SkillTarget, dest: Option<PathBuf>, dry_run: bool) -> Skil
556
578
  #[test]
557
579
  fn install_skill_dest_with_target_all_is_invalid() {
558
580
  // commands.py:453-454 — `--dest cannot be combined with --target all`.
559
- let opts = skill_opts(
560
- SkillTarget::All,
561
- Some(PathBuf::from("/custom/dest")),
562
- false,
563
- );
581
+ let opts = skill_opts(SkillTarget::All, Some(PathBuf::from("/custom/dest")), false);
564
582
  let err = install_skill(&opts).expect_err("dest + all must error");
565
583
  match err {
566
584
  PackagingError::InvalidOptions(msg) => assert!(
@@ -620,7 +638,11 @@ fn install_skill_dry_run_explicit_dest_single_target() {
620
638
  #[ignore = "REAL-MACHINE-E2E: real copytree + stale diff removal (fixes dirs_exist_ok=True residue)"]
621
639
  fn install_skill_real_copy_removes_stale_files() {
622
640
  // 修 commands.py:480 dirs_exist_ok 残留:Rust 拷前清旧 SKILL,记录 removed_stale.
623
- let opts = skill_opts(SkillTarget::Codex, Some(PathBuf::from("/tmp/ta-skill-real")), false);
641
+ let opts = skill_opts(
642
+ SkillTarget::Codex,
643
+ Some(PathBuf::from("/tmp/ta-skill-real")),
644
+ false,
645
+ );
624
646
  let outcomes = install_skill(&opts).expect("real install-skill");
625
647
  assert!(!outcomes[0].dry_run);
626
648
  // 真路径下若有旧残留,removed_stale 非空 (具体值依 fixture).
@@ -815,12 +837,33 @@ fn atomic_replace_outcome_serde_tag_outcome() {
815
837
  // ───────────────────────────────────────────────────────────────────────
816
838
 
817
839
  #[test]
840
+ #[serial_test::serial(env)]
818
841
  fn install_skill_dry_run_is_pure_no_provider_state() {
819
842
  // §84:install-skill 只拷文件;dry-run 连文件都不动 → 纯函数式可重复.
843
+ //
844
+ // 0.5.43 debt-sweep (§6.2): even dry-run reads ambient HOME to
845
+ // build the target skill path. Parallel real-copy sibling tests
846
+ // hold the same `ENV_LOCK_PKG` + `HomeGuard::set` guards; without
847
+ // matching guards here, two dry-run runs can observe HOME after
848
+ // a real-copy test swapped it, producing spurious diffs. Same
849
+ // env critical section as install/uninstall.
850
+ let _g = ENV_LOCK_PKG.lock().unwrap_or_else(|p| p.into_inner());
851
+ let home = std::env::temp_dir().join(format!(
852
+ "ta-0543-pkg-dry-{}-{}",
853
+ std::process::id(),
854
+ line!()
855
+ ));
856
+ let _ = std::fs::remove_dir_all(&home);
857
+ std::fs::create_dir_all(&home).expect("create dry-run HOME");
858
+ let _h = HomeGuard::set(&home);
820
859
  let opts = skill_opts(SkillTarget::Claude, None, true);
821
860
  let first = install_skill(&opts).expect("dry-run 1");
822
861
  let second = install_skill(&opts).expect("dry-run 2");
823
- assert_eq!(first, second, "dry-run install-skill must be deterministic & side-effect free");
862
+ assert_eq!(
863
+ first, second,
864
+ "dry-run install-skill must be deterministic & side-effect free"
865
+ );
866
+ let _ = std::fs::remove_dir_all(&home);
824
867
  }
825
868
 
826
869
  // ───────────────────────────────────────────────────────────────────────
@@ -892,7 +935,11 @@ fn install_skill_all_real_copies_to_three_provider_locations() {
892
935
  assert_eq!(outcomes.len(), 3, "all → codex+claude+copilot");
893
936
 
894
937
  for sub in [".codex", ".claude", ".copilot"] {
895
- let dest = home.join(sub).join("skills").join("team-agent").join("SKILL.md");
938
+ let dest = home
939
+ .join(sub)
940
+ .join("skills")
941
+ .join("team-agent")
942
+ .join("SKILL.md");
896
943
  assert!(dest.exists(), "{sub}: SKILL.md must exist after install");
897
944
  assert_eq!(
898
945
  std::fs::read(&dest).unwrap(),
@@ -51,8 +51,11 @@ pub enum SkillTarget {
51
51
  impl SkillTarget {
52
52
  /// `All` fan-out 的单目标全集(表驱动唯一真相源:新增 provider 只改这里,
53
53
  /// install/uninstall/install-skill 三处共用,杜绝漏装/漏卸)。
54
- pub const SINGLE_TARGETS: [SkillTarget; 3] =
55
- [SkillTarget::Codex, SkillTarget::Claude, SkillTarget::Copilot];
54
+ pub const SINGLE_TARGETS: [SkillTarget; 3] = [
55
+ SkillTarget::Codex,
56
+ SkillTarget::Claude,
57
+ SkillTarget::Copilot,
58
+ ];
56
59
 
57
60
  /// 单目标 → 对应 provider(`All` 无单一 provider → `None`)。与 [`Provider`] 对齐,防散字符串再生。
58
61
  pub fn provider(self) -> Option<Provider> {
@@ -67,9 +70,15 @@ impl SkillTarget {
67
70
  /// `_skill_dest_dir`:`~/.codex|.claude|.copilot/skills/team-agent`(`All` fan-out 全集,非单 dir → None)。
68
71
  pub fn dest_dir(self, home: &Path) -> Option<SkillDestDir> {
69
72
  match self {
70
- Self::Codex => Some(SkillDestDir(home.join(".codex").join("skills").join("team-agent"))),
71
- Self::Claude => Some(SkillDestDir(home.join(".claude").join("skills").join("team-agent"))),
72
- Self::Copilot => Some(SkillDestDir(home.join(".copilot").join("skills").join("team-agent"))),
73
+ Self::Codex => Some(SkillDestDir(
74
+ home.join(".codex").join("skills").join("team-agent"),
75
+ )),
76
+ Self::Claude => Some(SkillDestDir(
77
+ home.join(".claude").join("skills").join("team-agent"),
78
+ )),
79
+ Self::Copilot => Some(SkillDestDir(
80
+ home.join(".copilot").join("skills").join("team-agent"),
81
+ )),
73
82
  Self::All => None,
74
83
  }
75
84
  }
@@ -240,7 +249,10 @@ pub enum PathHint {
240
249
  /// bin 已在 PATH。
241
250
  OnPath { bin_dir: PathBuf },
242
251
  /// bin 不在 PATH —— 携带诊断(保留 WSL/`.npmrc` 等价提示)。// REAL-MACHINE-E2E: 真探 PATH。
243
- NotOnPath { bin_dir: PathBuf, diagnostic: PathDiagnostic },
252
+ NotOnPath {
253
+ bin_dir: PathBuf,
254
+ diagnostic: PathDiagnostic,
255
+ },
244
256
  }
245
257
 
246
258
  /// 「bin 不在 PATH」诊断(`bincheck.mjs:39-65` 自由 console.error → typed struct)。
@@ -280,7 +292,10 @@ pub enum AtomicReplaceOutcome {
280
292
  /// 跨卷 `EXDEV` → copy+fsync+rename fallback 成功。// REAL-MACHINE-E2E: 需跨卷真机。
281
293
  ReplacedCrossDevice { backup: PathBuf },
282
294
  /// 替换失败 → 已回滚到 `.previous`(原 dest 仍可用)。// REAL-MACHINE-E2E.
283
- RolledBack { restored_from: PathBuf, error: String },
295
+ RolledBack {
296
+ restored_from: PathBuf,
297
+ error: String,
298
+ },
284
299
  }
285
300
 
286
301
  /// `uninstall` 结果(`install.mjs:109-130`)。默认保留 runtime/workspace(有 team 在跑勿 purge)。
@@ -299,7 +299,10 @@ mod tests {
299
299
  // (windows) — both must resolve our own ppid.
300
300
  let my = std::process::id();
301
301
  let ppid = parent_pid(my);
302
- assert!(ppid.is_some(), "parent_pid must resolve own pid on both unix and windows");
302
+ assert!(
303
+ ppid.is_some(),
304
+ "parent_pid must resolve own pid on both unix and windows"
305
+ );
303
306
  assert!(ppid.unwrap() > 0);
304
307
  }
305
308
 
@@ -263,10 +263,7 @@ pub enum LockError {
263
263
  /// Existing product callers do NOT use this — they need their own
264
264
  /// metadata/waiter/held_long event machinery. This wrapper exists for
265
265
  /// simple future callers.
266
- pub fn try_lock_exclusive(
267
- path: &Path,
268
- timeout: Duration,
269
- ) -> Result<FileLockGuard, LockError> {
266
+ pub fn try_lock_exclusive(path: &Path, timeout: Duration) -> Result<FileLockGuard, LockError> {
270
267
  let file = File::options()
271
268
  .read(true)
272
269
  .write(true)
@@ -399,20 +396,37 @@ mod tests {
399
396
  #[test]
400
397
  fn timeout_error_carries_path_and_seconds() {
401
398
  // Convenience wrapper's timeout error shape.
402
- let dir = std::env::temp_dir().join("ta-b2-timeout");
399
+ //
400
+ // 0.5.43 debt-sweep (§6.2): the pre-0.5.43 fixed
401
+ // `ta-b2-timeout/timeout-lock.tmp` path let parallel test
402
+ // workers race for the same file across cargo threads. Each
403
+ // run now allocates a per-process + monotonic-atomic dir so
404
+ // `--test-threads=2` cannot false-fail. Timeout shape and
405
+ // `timeout-lock.tmp` basename are preserved so downstream
406
+ // guards keep firing.
407
+ static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
408
+ let dir = std::env::temp_dir().join(format!(
409
+ "ta-b2-timeout-{}-{}",
410
+ std::process::id(),
411
+ N.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
412
+ ));
403
413
  std::fs::create_dir_all(&dir).unwrap();
404
414
  let path = dir.join("timeout-lock.tmp");
405
- let _hold = try_lock_exclusive(&path, Duration::from_secs(1))
406
- .expect("first acquire must succeed");
415
+ let _hold =
416
+ try_lock_exclusive(&path, Duration::from_secs(1)).expect("first acquire must succeed");
407
417
  let err = try_lock_exclusive(&path, Duration::from_millis(150))
408
418
  .expect_err("second acquire must timeout");
409
419
  match err {
410
- LockError::Timeout { timeout_secs, path: p } => {
420
+ LockError::Timeout {
421
+ timeout_secs,
422
+ path: p,
423
+ } => {
411
424
  assert!(timeout_secs > 0.0);
412
425
  assert!(p.ends_with("timeout-lock.tmp"), "path suffix: {p}");
413
426
  }
414
427
  other => panic!("expected Timeout, got {other:?}"),
415
428
  }
416
429
  let _ = std::fs::remove_file(&path);
430
+ let _ = std::fs::remove_dir_all(&dir);
417
431
  }
418
432
  }